mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: restore hot reloading for 0.7 (#2775)
This commit is contained in:
parent
5657abc07d
commit
7b62ad44d2
13 changed files with 252 additions and 66 deletions
|
@ -72,6 +72,7 @@ pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
|
|||
<Route path=path!("/:id") view=Contact/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
|
@ -17,6 +17,7 @@ cfg-if = "1.0"
|
|||
hydration_context = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_server = { workspace = true, features = ["tachys"] }
|
||||
leptos_config = { workspace = true }
|
||||
|
|
|
@ -168,7 +168,7 @@ mod tests {
|
|||
fn creates_list() {
|
||||
Owner::new().with(|| {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: HtmlElement<_, _, _, Dom> = view! {
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<ol>
|
||||
<For each=move || values.get() key=|i| *i let:i>
|
||||
<li>{i}</li>
|
||||
|
@ -187,7 +187,7 @@ mod tests {
|
|||
fn creates_list_enumerate() {
|
||||
Owner::new().with(|| {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: HtmlElement<_, _, _, Dom> = view! {
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<ol>
|
||||
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
|
||||
<li>{move || index.get()}"-"{i}</li>
|
||||
|
@ -200,7 +200,7 @@ mod tests {
|
|||
<!>-<!>4</li><li>4<!>-<!>5</li><!></ol>"
|
||||
);
|
||||
|
||||
let list: HtmlElement<_, _, _, Dom> = view! {
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<ol>
|
||||
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
|
||||
<li>{move || index.get()}"-"{i}</li>
|
||||
|
|
|
@ -24,8 +24,13 @@ pub fn AutoReload(
|
|||
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
|
||||
};
|
||||
|
||||
let script = include_str!("reload_script.js");
|
||||
view! { <script nonce=nonce>{format!("{script}({reload_port:?}, {protocol})")}</script> }
|
||||
let script = format!(
|
||||
"(function (reload_port, protocol) {{ {} {} }})({reload_port:?}, \
|
||||
{protocol})",
|
||||
leptos_hot_reload::HOT_RELOAD_JS,
|
||||
include_str!("reload_script.js")
|
||||
);
|
||||
view! { <script nonce=nonce>{script}</script> }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
(function (reload_port, protocol) {
|
||||
let host = window.location.hostname;
|
||||
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
|
||||
ws.onmessage = (ev) => {
|
||||
|
@ -20,4 +19,3 @@ ws.onmessage = (ev) => {
|
|||
}
|
||||
};
|
||||
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
|
||||
})
|
||||
|
|
|
@ -1,18 +1,50 @@
|
|||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::dom::Dom,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
ssr::StreamBuilder,
|
||||
view::{add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
ToTemplate,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct View<T>(T)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct View<T>
|
||||
where
|
||||
T: Sized;
|
||||
T: Sized,
|
||||
{
|
||||
inner: T,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl<T> View<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
self.inner
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn with_view_marker(
|
||||
#[allow(unused_mut)] // used in debug
|
||||
mut self,
|
||||
#[allow(unused_variables)] // used in debug
|
||||
view_marker: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.view_marker = Some(view_marker.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,33 +60,37 @@ where
|
|||
T: Sized + Render<Dom> + RenderHtml<Dom> + Send, //+ AddAnyAttr<Dom>,
|
||||
{
|
||||
fn into_view(self) -> View<Self> {
|
||||
View(self)
|
||||
View {
|
||||
inner: self,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView> Render<Dom> for View<T> {
|
||||
impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
|
||||
type State = T::State;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
self.0.build()
|
||||
self.inner.build()
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.0.rebuild(state)
|
||||
self.inner.rebuild(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView> RenderHtml<Dom> for View<T> {
|
||||
impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml<Dom>>::MIN_LENGTH;
|
||||
const MIN_LENGTH: usize = <T as RenderHtml<Rndr>>::MIN_LENGTH;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self.0.resolve().await
|
||||
self.inner.resolve().await
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
self.0.dry_resolve();
|
||||
self.inner.dry_resolve();
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
|
@ -64,8 +100,20 @@ impl<T: IntoView> RenderHtml<Dom> for View<T> {
|
|||
escape: bool,
|
||||
mark_branches: bool,
|
||||
) {
|
||||
self.0
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
|
||||
self.inner
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
|
@ -77,35 +125,70 @@ impl<T: IntoView> RenderHtml<Dom> for View<T> {
|
|||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
self.0.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
|
||||
self.inner.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
)
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
buf.push_sync(&format!("<!--hot-reload|{vm}|close-->"));
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Dom>,
|
||||
cursor: &Cursor<Rndr>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
self.0.hydrate::<FROM_SERVER>(cursor, position)
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView> AddAnyAttr<Dom> for View<T> {
|
||||
type Output<SomeNewAttr: Attribute<Dom>> =
|
||||
<T as AddAnyAttr<Dom>>::Output<SomeNewAttr>;
|
||||
impl<T: ToTemplate> ToTemplate for View<T> {
|
||||
fn to_template(
|
||||
buf: &mut String,
|
||||
class: &mut String,
|
||||
style: &mut String,
|
||||
inner_html: &mut String,
|
||||
position: &mut Position,
|
||||
) {
|
||||
T::to_template(buf, class, style, inner_html, position);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
impl<T: AddAnyAttr<Rndr>, Rndr> AddAnyAttr<Rndr> for View<T>
|
||||
where
|
||||
Rndr: Renderer,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = View<T::Output<SomeNewAttr>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
self.0.add_any_attr(attr)
|
||||
let View {
|
||||
inner,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
} = self;
|
||||
View {
|
||||
inner: inner.add_any_attr(attr),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{logging, IntoView};
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::logging;
|
||||
use crate::IntoView;
|
||||
use any_spawner::Executor;
|
||||
use reactive_graph::owner::Owner;
|
||||
#[cfg(debug_assertions)]
|
||||
|
|
|
@ -5,7 +5,7 @@ fn simple_ssr_test() {
|
|||
use leptos::prelude::*;
|
||||
|
||||
let (value, set_value) = signal(0);
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
|
@ -36,7 +36,7 @@ fn ssr_test_with_components() {
|
|||
}
|
||||
}
|
||||
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<div class="counters">
|
||||
<Counter initial_value=1/>
|
||||
<Counter initial_value=2/>
|
||||
|
@ -66,7 +66,7 @@ fn ssr_test_with_snake_case_components() {
|
|||
</div>
|
||||
}
|
||||
}
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<div class="counters">
|
||||
<SnakeCaseCounter initial_value=1/>
|
||||
<SnakeCaseCounter initial_value=2/>
|
||||
|
@ -86,7 +86,7 @@ fn test_classes() {
|
|||
use leptos::prelude::*;
|
||||
|
||||
let (value, _set_value) = signal(5);
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
<div
|
||||
class="my big"
|
||||
class:a=move || { value.get() > 10 }
|
||||
|
@ -104,7 +104,7 @@ fn ssr_with_styles() {
|
|||
|
||||
let (_, set_value) = signal(0);
|
||||
let styles = "myclass";
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! { class=styles,
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! { class=styles,
|
||||
<div>
|
||||
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>
|
||||
"-1"
|
||||
|
@ -124,7 +124,7 @@ fn ssr_option() {
|
|||
use leptos::prelude::*;
|
||||
|
||||
let (_, _) = signal(0);
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! { <option></option> };
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! { <option></option> };
|
||||
|
||||
assert_eq!(rendered.to_html(), "<option></option>");
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
console.log("[HOT RELOADING] Connected to server.");
|
||||
console.log("[HOT RELOADING] Connected to server.\n\nNote: `cargo-leptos watch --hot-reload` only works with the `nightly` feature enabled on Leptos.");
|
||||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
console.log("[HOT RELOAD]", id, patches);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
open = `leptos-view|${id}|open`,
|
||||
close = `leptos-view|${id}|close`;
|
||||
open = `hot-reload|${id}|open`,
|
||||
close = `hot-reload|${id}|close`;
|
||||
let start, end;
|
||||
const instances = [];
|
||||
while (walker.nextNode()) {
|
||||
|
@ -259,11 +259,11 @@ function patch(json) {
|
|||
node: walker.currentNode,
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("hot-reload")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
|
||||
const startingName = walker.currentNode.textContent.trim();
|
||||
const componentName = startingName.replace("-children|open").replace("leptos-view|");
|
||||
const endingName = `leptos-view|${componentName}-children|close`;
|
||||
const componentName = startingName.replace("-children|open").replace("hot-reload|");
|
||||
const endingName = `hot-reload|${componentName}-children|close`;
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
|
|
|
@ -302,7 +302,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
|||
let parser = rstml::Parser::new(config);
|
||||
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
|
||||
let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens());
|
||||
let nodes_output = view::render_view(&nodes, global_class.as_ref(), None);
|
||||
let nodes_output = view::render_view(
|
||||
&nodes,
|
||||
global_class.as_ref(),
|
||||
normalized_call_site(proc_macro::Span::call_site()),
|
||||
);
|
||||
quote! {
|
||||
{
|
||||
#(#errors;)*
|
||||
|
@ -312,6 +316,20 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
|||
.into()
|
||||
}
|
||||
|
||||
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, feature = "nightly"))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.source_file().path(),
|
||||
site.start().line()
|
||||
))
|
||||
} else {
|
||||
_ = site;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
|
|
|
@ -30,28 +30,60 @@ pub fn render_view(
|
|||
global_class: Option<&TokenTree>,
|
||||
view_marker: Option<String>,
|
||||
) -> Option<TokenStream> {
|
||||
match nodes.len() {
|
||||
let (base, should_add_view) = match nodes.len() {
|
||||
0 => {
|
||||
let span = Span::call_site();
|
||||
Some(quote_spanned! {
|
||||
span => ()
|
||||
})
|
||||
(
|
||||
Some(quote_spanned! {
|
||||
span => ()
|
||||
}),
|
||||
false,
|
||||
)
|
||||
}
|
||||
1 => node_to_tokens(
|
||||
&nodes[0],
|
||||
TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
view_marker.as_deref(),
|
||||
1 => (
|
||||
node_to_tokens(
|
||||
&nodes[0],
|
||||
TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
view_marker.as_deref(),
|
||||
),
|
||||
// only add View wrapper and view marker to a regular HTML
|
||||
// element or component, not to a <{..} /> attribute list
|
||||
match &nodes[0] {
|
||||
Node::Element(node) => !is_spread_marker(node),
|
||||
_ => false,
|
||||
},
|
||||
),
|
||||
_ => fragment_to_tokens(
|
||||
nodes,
|
||||
TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
view_marker.as_deref(),
|
||||
_ => (
|
||||
fragment_to_tokens(
|
||||
nodes,
|
||||
TagType::Unknown,
|
||||
None,
|
||||
global_class,
|
||||
view_marker.as_deref(),
|
||||
),
|
||||
true,
|
||||
),
|
||||
}
|
||||
};
|
||||
base.map(|view| {
|
||||
if !should_add_view {
|
||||
view
|
||||
} else if let Some(vm) = view_marker {
|
||||
quote! {
|
||||
::leptos::prelude::View::new(
|
||||
#view
|
||||
)
|
||||
.with_view_marker(#vm)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::prelude::View::new(
|
||||
#view
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn element_children_to_tokens(
|
||||
|
|
|
@ -345,7 +345,9 @@ where
|
|||
Unsuspend::new(move || match condition {
|
||||
Some(true) => Either::Left(view()),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => Either::Right(view! { <Redirect path=redirect_path()/> }),
|
||||
Some(false) => {
|
||||
Either::Right(view! { <Redirect path=redirect_path()/> }.into_inner())
|
||||
}
|
||||
None => Either::Right(()),
|
||||
})
|
||||
}}
|
||||
|
@ -390,7 +392,9 @@ where
|
|||
match condition() {
|
||||
Some(true) => Either::Left(view()),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => Either::Right(view! { <Redirect path=redirect_path()/> }),
|
||||
Some(false) => {
|
||||
Either::Right(view! { <Redirect path=redirect_path()/> }.into_inner())
|
||||
}
|
||||
None => Either::Right(()),
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -92,11 +92,53 @@ impl Renderer for Dom {
|
|||
}
|
||||
|
||||
fn first_child(node: &Self::Node) -> Option<Self::Node> {
|
||||
node.first_child()
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let node = node.first_child();
|
||||
// if it's a comment node that starts with hot-reload, it's a marker that should be
|
||||
// ignored
|
||||
if let Some(node) = node.as_ref() {
|
||||
if node.node_type() == 8
|
||||
&& node
|
||||
.text_content()
|
||||
.unwrap_or_default()
|
||||
.starts_with("hot-reload")
|
||||
{
|
||||
return Self::next_sibling(node);
|
||||
}
|
||||
}
|
||||
|
||||
node
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
node.first_child()
|
||||
}
|
||||
}
|
||||
|
||||
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
|
||||
node.next_sibling()
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let node = node.next_sibling();
|
||||
// if it's a comment node that starts with hot-reload, it's a marker that should be
|
||||
// ignored
|
||||
if let Some(node) = node.as_ref() {
|
||||
if node.node_type() == 8
|
||||
&& node
|
||||
.text_content()
|
||||
.unwrap_or_default()
|
||||
.starts_with("hot-reload")
|
||||
{
|
||||
return Self::next_sibling(node);
|
||||
}
|
||||
}
|
||||
|
||||
node
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
node.next_sibling()
|
||||
}
|
||||
}
|
||||
|
||||
fn log_node(node: &Self::Node) {
|
||||
|
|
Loading…
Reference in a new issue