Compare commits

..

9 Commits

Author SHA1 Message Date
Greg Johnston
123b1ef24d docs: clarify difference between set() and update() 2023-05-22 15:33:55 -04:00
Greg Johnston
2d418dae93 fix: debug-mode bugs in <For/> (closes #955, #1075, #1076) (#1078) 2023-05-22 06:49:13 -04:00
Greg Johnston
91e0fcdc1b fix/change: remove ? prefix from search in browser (matching server behavior) - closes #1071 (#1077) 2023-05-21 22:06:38 -04:00
Greg Johnston
a9ed8461d1 feat: add "async routing" feature (#1055)
* add "async routing" feature that waits for async resources to resolve before navigating
* add support for Outlet
* add `<RoutingProgress/>` component
2023-05-21 06:46:23 -04:00
Vladimir Motylenko
5a71ca797a feat: RSX parser with recovery after errors, and unquoted text (#1054)
* Feat: Upgrade to new local version of syn-rsx

* chore: Make macro more IDE friendly

1. Add quotation to RawText node.
2. Replace vec! macro with [].to_vec().
Cons:
1. Temporary remove allow(unused_braces) from expressions, to allow completion after dot in rust-analyzer.

* chore: Change dependency from syn-rsx to rstml

* chore: Fix value_to_string usage, pr comments, and fmt.
2023-05-21 06:45:53 -04:00
agilarity
70eb07d7d6 test: setup e2e automatically (#1067) 2023-05-20 20:46:06 -04:00
Greg Johnston
71ee69af01 fix: avoid potential already-borrowed issues with resources nested in suspense 2023-05-20 20:42:06 -04:00
Ben Wishovich
dd41c0586c feat: allow specifying exact server function paths (#1069) 2023-05-19 16:47:28 -04:00
Greg Johnston
aaf63dbf5c docs: clarify SSR/WASM binary size comments (#1070) 2023-05-19 15:46:26 -04:00
33 changed files with 717 additions and 365 deletions

View File

@@ -90,7 +90,8 @@ view! { cx,
<button
// define an event listener with on:
on:click=move |_| {
set_count.update(|n| *n += 1);
// on stable, this is set_count.set(3);
set_count(3);
}
>
// text nodes are wrapped in quotation marks
@@ -142,6 +143,16 @@ in a function, telling the framework to update the view every time `count` chang
`{count()}` access the value of `count` once, and passes an `i32` into the view,
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
Lets make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Lets replacing “set this value to 3” with “increment this value by 1”:
```rust
move |_| {
set_count.update(|n| *n += 1);
}
```
You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
> Throughout this tutorial, well use CodeSandbox to show interactive examples. To
> show the browser in the sandbox, you may need to click `Add DevTools >
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details

View File

@@ -1,5 +1,5 @@
[tasks.web-test]
dependencies = ["cargo-leptos-e2e"]
dependencies = ["auto-setup", "cargo-leptos-e2e"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -78,3 +78,10 @@ script = '''
exit 1
fi
'''
[tasks.auto-setup]
script = '''
if [ ! -d "${END2END_DIR}/node_modules" ]; then
cargo make setup
fi
'''

View File

@@ -1,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView, Scope};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
@@ -9,23 +9,25 @@ use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<>
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
</Routes>
</main>
</Router>
</>
let (is_routing, set_is_routing) = create_signal(cx, false);
view! { cx,
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
// adding `set_is_routing` causes the router to wait for async data to load on new pages
<Router set_is_routing>
// shows a progress bar while async data are loading
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
</Routes>
</main>
</Router>
}
}

View File

@@ -278,7 +278,33 @@ where
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
match child {
View::CoreComponent(
crate::CoreComponent::DynChild(
child,
),
) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
View::Component(child) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
_ => unmount_child(&start, end),
}
}
// Mount the new child

View File

@@ -168,7 +168,7 @@ pub(crate) struct EachItem {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment: Option<web_sys::DocumentFragment>,
#[cfg(debug_assertions)]
opening: Comment,
opening: Option<Comment>,
pub(crate) child: View,
closing: Option<Comment>,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -201,7 +201,11 @@ impl EachItem {
None
},
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<EachItem>"), &id, false),
if needs_closing {
Some(Comment::new(Cow::Borrowed("<EachItem>"), &id, false))
} else {
None
},
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -215,7 +219,10 @@ impl EachItem {
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &closing.node)
.append_with_node_2(
&markers.1.as_ref().unwrap().node,
&closing.node,
)
.unwrap();
fragment.append_with_node_1(&closing.node).unwrap();
}
@@ -260,10 +267,6 @@ impl Mountable for EachItem {
#[inline(always)]
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self.opening.node.clone();
#[cfg(not(debug_assertions))]
return self.child.get_opening_node();
}
@@ -673,10 +676,20 @@ fn apply_cmds<T, EF, N>(
// 4. Add
if cmds.clear {
cmds.removed.clear();
crate::log!("clearing list");
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str("open"),
opening,
);
web_sys::console::log_2(
&wasm_bindgen::JsValue::from_str("closing"),
closing,
);
if opening.previous_sibling().is_none()
&& closing.next_sibling().is_none()
{
crate::log!("no siblings");
let parent = closing
.parent_node()
.expect("could not get closing node")
@@ -689,6 +702,7 @@ fn apply_cmds<T, EF, N>(
#[cfg(not(debug_assertions))]
parent.append_with_node_1(closing).unwrap();
} else {
crate::log!("yes siblings");
range.set_start_before(opening).unwrap();
range.set_end_before(closing).unwrap();

View File

@@ -11,7 +11,7 @@ readme = "../README.md"
[dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
syn = { version = "1", features = [
syn = { version = "2", features = [
"full",
"parsing",
"extra-traits",
@@ -19,7 +19,7 @@ syn = { version = "1", features = [
"printing",
] }
quote = "1"
syn-rsx = "0.9"
rstml = "0.10.6"
proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
parking_lot = "0.12"
walkdir = "2"

View File

@@ -76,7 +76,7 @@ impl ViewMacros {
tokens.next(); // ,
// TODO handle class = ...
let rsx =
syn_rsx::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
let template = LNode::parse_view(rsx)?;
views.push(MacroInvocation { id, template })
}

View File

@@ -1,8 +1,8 @@
use crate::parsing::{is_component_node, value_to_string};
use crate::parsing::is_component_node;
use anyhow::Result;
use quote::quote;
use quote::ToTokens;
use rstml::node::{Node, NodeAttribute};
use serde::{Deserialize, Serialize};
use syn_rsx::Node;
// A lightweight virtual DOM structure we can use to hold
// the state of a Leptos view macro template. This is because
@@ -58,36 +58,30 @@ impl LNode {
}
}
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
views.push(LNode::Text(value));
} else {
let value = text.value.as_ref();
let code = quote! { #value };
let code = code.to_string();
views.push(LNode::DynChild(code));
}
views.push(LNode::Text(text.value_string()));
}
Node::Block(block) => {
let value = block.value.as_ref();
let code = quote! { #value };
let code = block.into_token_stream();
let code = code.to_string();
views.push(LNode::DynChild(code));
}
Node::Element(el) => {
if is_component_node(&el) {
let name = el.name().to_string();
let mut children = Vec::new();
for child in el.children {
LNode::parse_node(child, &mut children)?;
}
views.push(LNode::Component {
name: el.name.to_string(),
name: name,
props: el
.open_tag
.attributes
.into_iter()
.filter_map(|attr| match attr {
Node::Attribute(attr) => Some((
NodeAttribute::Attribute(attr) => Some((
attr.key.to_string(),
format!("{:#?}", attr.value),
format!("{:#?}", attr.value()),
)),
_ => None,
})
@@ -95,15 +89,13 @@ impl LNode {
children,
});
} else {
let name = el.name.to_string();
let name = el.name().to_string();
let mut attrs = Vec::new();
for attr in el.attributes {
if let Node::Attribute(attr) = attr {
for attr in el.open_tag.attributes {
if let NodeAttribute::Attribute(attr) = attr {
let name = attr.key.to_string();
if let Some(value) =
attr.value.as_ref().and_then(value_to_string)
{
if let Some(value) = attr.value_literal_string() {
attrs.push((
name,
LAttributeValue::Static(value),

View File

@@ -1,7 +1,37 @@
use syn_rsx::{NodeElement, NodeValueExpr};
use rstml::node::NodeElement;
pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
match &value.as_ref() {
///
/// Converts `syn::Block` to simple expression
///
/// For example:
/// ```no_build
/// // "string literal" in
/// {"string literal"}
/// // number literal
/// {0x12}
/// // boolean literal
/// {true}
/// // variable
/// {path::x}
/// ```
pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
// its empty block, or block with multi lines
if block.stmts.len() != 1 {
return None;
}
match &block.stmts[0] {
syn::Stmt::Expr(e, None) => return Some(&e),
_ => {}
}
None
}
/// Converts simple literals to its string representation.
///
/// This function doesn't convert literal wrapped inside block
/// like: `{"string"}`.
pub fn value_to_string(value: &syn::Expr) -> Option<String> {
match &value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
@@ -14,7 +44,7 @@ pub fn value_to_string(value: &NodeValueExpr) -> Option<String> {
}
pub fn is_component_node(node: &NodeElement) -> bool {
node.name
node.name()
.to_string()
.starts_with(|c: char| c.is_ascii_uppercase())
}

View File

@@ -12,16 +12,16 @@ readme = "../README.md"
proc-macro = true
[dependencies]
attribute-derive = { version = "0.5", features = ["syn-full"] }
attribute-derive = { version = "0.6", features = ["syn-full"] }
cfg-if = "1"
html-escape = "0.2"
itertools = "0.10"
prettyplease = "0.1"
prettyplease = "0.2.4"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full"] }
syn-rsx = "0.9"
syn = { version = "2", features = ["full"] }
rstml = "0.10.6"
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"

View File

@@ -4,15 +4,15 @@ use convert_case::{
Casing,
};
use itertools::Itertools;
use leptos_hot_reload::parsing::value_to_string;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, spanned::Spanned,
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, Stmt,
Type, TypePath, Visibility,
};
pub struct Model {
is_transparent: bool,
docs: Docs,
@@ -56,14 +56,17 @@ impl Parse for Model {
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
// attrs from the function signature
drain_filter(&mut item.attrs, |attr| {
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
drain_filter(&mut item.attrs, |attr| match &attr.meta {
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
Meta::List(attr) => attr.path == parse_quote!(prop),
_ => false,
});
item.sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
drain_filter(&mut ty.attrs, |attr| {
attr.path == parse_quote!(doc)
|| attr.path == parse_quote!(prop)
drain_filter(&mut ty.attrs, |attr| match &attr.meta {
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
Meta::List(attr) => attr.path == parse_quote!(prop),
_ => false,
});
}
});
@@ -400,12 +403,20 @@ impl Docs {
let mut attrs = attrs
.iter()
.filter_map(|attr| attr.path.is_ident("doc").then(|| {
let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else {
abort!(attr, "expected doc comment to be string literal");
.filter_map(|attr| {
let Meta::NameValue(attr ) = &attr.meta else {
return None
};
(doc.value(), doc.span())
}))
if !attr.path.is_ident("doc") {
return None
}
let Some(val) = value_to_string(&attr.value) else {
abort!(attr, "expected string literal in value of doc comment");
};
Some((val, attr.path.span()))
})
.flat_map(map)
.collect_vec();

View File

@@ -7,9 +7,9 @@ extern crate proc_macro_error;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use rstml::{node::KeyedAttribute, parse};
use server_fn_macro::{server_macro_impl, ServerContext};
use syn::parse_macro_input;
use syn_rsx::{parse, NodeAttribute};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
@@ -351,16 +351,22 @@ pub fn view(tokens: TokenStream) -> TokenStream {
.chain(tokens)
.collect()
};
match parse(tokens.into()) {
Ok(nodes) => render_view(
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
Mode::default(),
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
),
Err(error) => error.to_compile_error(),
let config = rstml::ParserConfig::default().recover_block(true);
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 = render_view(
&cx,
&nodes,
Mode::default(),
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
);
quote! {
{
#(#errors;)*
#nodes_output
}
}
.into()
}
@@ -874,9 +880,9 @@ pub fn params_derive(
}
}
pub(crate) fn attribute_value(attr: &NodeAttribute) -> &syn::Expr {
match &attr.value {
Some(value) => value.as_ref(),
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
match &attr.possible_value {
Some(value) => &value.value,
None => abort!(attr.key, "attribute should have value"),
}
}

View File

@@ -5,7 +5,8 @@ use attribute_derive::Attribute as AttributeDerive;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Type, Visibility,
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Meta, Type,
Visibility,
};
pub struct Model {
@@ -31,13 +32,16 @@ impl Parse for Model {
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
// attrs from the function signature
drain_filter(&mut item.attrs, |attr| {
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
drain_filter(&mut item.attrs, |attr| match &attr.meta {
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
Meta::List(attr) => attr.path == parse_quote!(prop),
_ => false,
});
item.fields.iter_mut().for_each(|arg| {
drain_filter(&mut arg.attrs, |attr| {
attr.path == parse_quote!(doc)
|| attr.path == parse_quote!(prop)
drain_filter(&mut arg.attrs, |attr| match &attr.meta {
Meta::NameValue(attr) => attr.path == parse_quote!(doc),
Meta::List(attr) => attr.path == parse_quote!(prop),
_ => false,
});
});

View File

@@ -1,9 +1,14 @@
use crate::attribute_value;
use leptos_hot_reload::parsing::is_component_node;
use itertools::Either;
use leptos_hot_reload::parsing::{
block_to_primitive_expression, is_component_node, value_to_string,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
};
use syn::spanned::Spanned;
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
use uuid::Uuid;
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
@@ -53,7 +58,7 @@ fn root_element_to_tokens(
.unwrap();
};
let span = node.name.span();
let span = node.name().span();
let navigations = if navigations.is_empty() {
quote! {}
@@ -67,7 +72,7 @@ fn root_element_to_tokens(
quote! { #(#expressions;);* }
};
let tag_name = node.name.to_string();
let tag_name = node.name().to_string();
quote_spanned! {
span => {
@@ -104,9 +109,9 @@ enum PrevSibChange {
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node.attributes.iter().filter_map(|node| {
if let Node::Attribute(attribute) = node {
fn attributes(node: &NodeElement) -> impl Iterator<Item = &KeyedAttribute> {
node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(attribute) = node {
Some(attribute)
} else {
None
@@ -129,11 +134,11 @@ fn element_to_tokens(
) -> Ident {
// create this element
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node.name.span());
let this_el_ident = child_ident(*next_el_id, node.name().span());
// Open tag
let name_str = node.name.to_string();
let span = node.name.span();
let name_str = node.name().to_string();
let span = node.name().span();
// CSR/hydrate, push to template
template.push('<');
@@ -145,7 +150,7 @@ fn element_to_tokens(
}
// navigation for this el
let debug_name = node.name.to_string();
let debug_name = node.name().to_string();
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
@@ -247,14 +252,17 @@ fn next_sibling_node(
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Ok(Some(child_ident(*next_el_id + 1, sibling.name.span())))
Ok(Some(child_ident(
*next_el_id + 1,
sibling.name().span(),
)))
}
}
Node::Block(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
Node::Text(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.value.span())))
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
_ => Err("expected either an element or a block".to_string()),
}
@@ -263,7 +271,7 @@ fn next_sibling_node(
fn attr_to_tokens(
cx: &Ident,
node: &NodeAttribute,
node: &KeyedAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
@@ -272,8 +280,8 @@ fn attr_to_tokens(
let name = name.strip_prefix('_').unwrap_or(&name);
let name = name.strip_prefix("attr:").unwrap_or(name);
let value = match &node.value {
Some(expr) => match expr.as_ref() {
let value = match &node.value() {
Some(expr) => match expr {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
@@ -367,7 +375,7 @@ fn child_to_tokens(
Node::Element(node) => {
if is_component_node(node) {
proc_macro_error::emit_error!(
node.name.span(),
node.name().span(),
"component children not allowed in template!, use view! \
instead"
);
@@ -389,7 +397,7 @@ fn child_to_tokens(
}
Node::Text(node) => block_to_tokens(
cx,
&node.value,
Either::Left(node.value_string()),
node.value.span(),
parent,
prev_sib,
@@ -399,10 +407,42 @@ fn child_to_tokens(
expressions,
navigations,
),
Node::Block(node) => block_to_tokens(
Node::RawText(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
Either::Left(node.to_string_best()),
node.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::Block(NodeBlock::ValidBlock(b)) => {
let value = match block_to_primitive_expression(b)
.and_then(value_to_string)
{
Some(v) => Either::Left(v),
None => Either::Right(b.into_token_stream()),
};
block_to_tokens(
cx,
value,
b.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
)
}
Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens(
cx,
Either::Right(b.into_token_stream()),
b.span(),
parent,
prev_sib,
next_sib,
@@ -418,7 +458,7 @@ fn child_to_tokens(
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
_cx: &Ident,
value: &NodeValueExpr,
value: Either<String, TokenStream>,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
@@ -428,18 +468,6 @@ fn block_to_tokens(
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
) -> PrevSibChange {
let value = value.as_ref();
let str_value = match value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
};
// code to navigate to this text node
let (name, location) = /* if is_first_child && mode == Mode::Client {
@@ -473,27 +501,30 @@ fn block_to_tokens(
}
};
if let Some(v) = str_value {
navigations.push(location);
template.push_str(&v);
match value {
Either::Left(v) => {
navigations.push(location);
template.push_str(&v);
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
} else {
template.push_str("<!>");
navigations.push(location);
Either::Right(value) => {
template.push_str("<!>");
navigations.push(location);
expressions.push(quote! {
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
});
expressions.push(quote! {
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
});
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
}

View File

@@ -1,11 +1,15 @@
use crate::{attribute_value, Mode};
use convert_case::{Case::Snake, Casing};
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
use leptos_hot_reload::parsing::{
block_to_primitive_expression, is_component_node, value_to_string,
};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
#[derive(Clone, Copy)]
enum TagType {
@@ -213,18 +217,22 @@ fn root_node_to_tokens_ssr(
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
leptos::leptos_dom::html::text(#value)
leptos::leptos_dom::html::text(#node)
}
}
Node::RawText(r) => {
let text = r.to_string_best();
let text = syn::LitStr::new(&text, r.span());
quote! {
leptos::leptos_dom::html::text(#text)
}
}
Node::Block(node) => {
let value = node.value.as_ref();
quote! {
#[allow(unused_braces)]
#value
#node
}
}
Node::Element(node) => {
@@ -254,9 +262,9 @@ fn fragment_to_tokens_ssr(
});
quote! {
{
leptos::Fragment::lazy(|| vec![
leptos::Fragment::lazy(|| [
#(#nodes),*
])
].to_vec())
#view_marker
}
}
@@ -329,15 +337,15 @@ fn root_element_to_tokens_ssr(
},
});
let tag_name = node.name.to_string();
let tag_name = node.name().to_string();
let is_custom_element = is_custom_element(&tag_name);
let typed_element_name = if is_custom_element {
Ident::new("Custom", node.name.span())
Ident::new("Custom", node.name().span())
} else {
let camel_cased = camel_case_tag_name(
&tag_name.replace("svg::", "").replace("math::", ""),
);
Ident::new(&camel_cased, node.name.span())
Ident::new(&camel_cased, node.name().span())
};
let typed_element_name = if is_svg_element(&tag_name) {
quote! { svg::#typed_element_name }
@@ -409,7 +417,7 @@ fn element_to_tokens_ssr(
}));
} else {
let tag_name = node
.name
.name()
.to_string()
.replace("svg::", "")
.replace("math::", "");
@@ -419,8 +427,8 @@ fn element_to_tokens_ssr(
let mut inner_html = None;
for attr in &node.attributes {
if let Node::Attribute(attr) = attr {
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
inner_html = attribute_to_tokens_ssr(
cx,
attr,
@@ -439,9 +447,9 @@ fn element_to_tokens_ssr(
quote! { leptos::leptos_dom::HydrationCtx::id() }
};
match node
.attributes
.attributes()
.iter()
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
{
Some(_) => {
template.push_str(" leptos-hk=\"_{}\"");
@@ -462,7 +470,7 @@ fn element_to_tokens_ssr(
if let Some(inner_html) = inner_html {
template.push_str("{}");
let value = inner_html.as_ref();
let value = inner_html;
holes.push(quote! {
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
@@ -484,32 +492,23 @@ fn element_to_tokens_ssr(
);
}
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
let value = if is_script_or_style {
value.into()
} else {
html_escape::encode_safe(&value)
};
template.push_str(
&value
.replace('{', "\\{")
.replace('}', "\\}"),
);
let value = text.value_string();
let value = if is_script_or_style {
value.into()
} else {
template.push_str("{}");
let value = text.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx)
})
}
html_escape::encode_safe(&value)
};
template.push_str(
&value.replace('{', "\\{").replace('}', "\\}"),
);
}
Node::Block(block) => {
if let Some(value) = value_to_string(&block.value) {
Node::Block(NodeBlock::ValidBlock(block)) => {
if let Some(value) =
block_to_primitive_expression(block)
.and_then(value_to_string)
{
template.push_str(&value);
} else {
let value = block.value.as_ref();
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
@@ -517,10 +516,16 @@ fn element_to_tokens_ssr(
})
}
chunks.push(SsrElementChunks::View(quote! {
{#value}.into_view(#cx)
{#block}.into_view(#cx)
}));
}
}
// Keep invalid blocks for faster IDE diff (on user type)
Node::Block(block @ NodeBlock::Invalid { .. }) => {
chunks.push(SsrElementChunks::View(quote! {
{#block}.into_view(#cx)
}));
}
Node::Fragment(_) => abort!(
Span::call_site(),
"You can't nest a fragment inside an element."
@@ -531,7 +536,7 @@ fn element_to_tokens_ssr(
}
template.push_str("</");
template.push_str(&node.name.to_string());
template.push_str(&node.name().to_string());
template.push('>');
}
}
@@ -540,17 +545,17 @@ fn element_to_tokens_ssr(
// returns `inner_html`
fn attribute_to_tokens_ssr<'a>(
cx: &Ident,
node: &'a NodeAttribute,
attr: &'a KeyedAttribute,
template: &mut String,
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) -> Option<&'a NodeValueExpr> {
let name = node.key.to_string();
) -> Option<&'a syn::Expr> {
let name = attr.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
// ignore refs on SSR
} else if let Some(name) = name.strip_prefix("on:") {
let handler = attribute_value(node);
let handler = attribute_value(attr);
let (event_type, _, _) = parse_event_name(name);
exprs_for_compiler.push(quote! {
@@ -563,16 +568,16 @@ fn attribute_to_tokens_ssr<'a>(
// ignore props for SSR
// ignore classes and sdtyles: we'll handle these separately
} else if name == "inner_html" {
return node.value.as_ref();
return attr.value();
} else {
let name = name.replacen("attr:", "", 1);
// special case of global_class and class attribute
if name == "class"
&& global_class.is_some()
&& node.value.as_ref().and_then(value_to_string).is_none()
&& attr.value().and_then(value_to_string).is_none()
{
let span = node.key.span();
let span = attr.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
@@ -582,7 +587,7 @@ fn attribute_to_tokens_ssr<'a>(
if name != "class" && name != "style" {
template.push(' ');
if let Some(value) = node.value.as_ref() {
if let Some(value) = attr.value() {
if let Some(value) = value_to_string(value) {
template.push_str(&name);
template.push_str("=\"");
@@ -590,7 +595,6 @@ fn attribute_to_tokens_ssr<'a>(
template.push('"');
} else {
template.push_str("{}");
let value = value.as_ref();
holes.push(quote! {
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
@@ -630,11 +634,13 @@ fn set_class_attribute_ssr(
Some(val) => (String::new(), Some(val)),
};
let static_class_attr = node
.attributes
.attributes()
.iter()
.filter_map(|a| match a {
Node::Attribute(attr) if attr.key.to_string() == "class" => {
attr.value.as_ref().and_then(value_to_string)
NodeAttribute::Attribute(attr)
if attr.key.to_string() == "class" =>
{
attr.value().and_then(value_to_string)
}
_ => None,
})
@@ -644,17 +650,17 @@ fn set_class_attribute_ssr(
.join(" ");
let dyn_class_attr = node
.attributes
.attributes()
.iter()
.filter_map(|a| {
if let Node::Attribute(a) = a {
if let NodeAttribute::Attribute(a) = a {
if a.key.to_string() == "class" {
if a.value.as_ref().and_then(value_to_string).is_some()
if a.value().and_then(value_to_string).is_some()
|| fancy_class_name(&a.key.to_string(), cx, a).is_some()
{
None
} else {
Some((a.key.span(), &a.value))
Some((a.key.span(), a.value()))
}
} else {
None
@@ -666,10 +672,10 @@ fn set_class_attribute_ssr(
.collect::<Vec<_>>();
let class_attrs = node
.attributes
.attributes()
.iter()
.filter_map(|node| {
if let Node::Attribute(node) = node {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if name == "class" {
return if let Some((_, name, value)) =
@@ -713,7 +719,6 @@ fn set_class_attribute_ssr(
for (_span, value) in dyn_class_attr {
if let Some(value) = value {
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
@@ -745,11 +750,13 @@ fn set_style_attribute_ssr(
holes: &mut Vec<TokenStream>,
) {
let static_style_attr = node
.attributes
.attributes()
.iter()
.filter_map(|a| match a {
Node::Attribute(attr) if attr.key.to_string() == "style" => {
attr.value.as_ref().and_then(value_to_string)
NodeAttribute::Attribute(attr)
if attr.key.to_string() == "style" =>
{
attr.value().and_then(value_to_string)
}
_ => None,
})
@@ -757,17 +764,17 @@ fn set_style_attribute_ssr(
.map(|style| format!("{style};"));
let dyn_style_attr = node
.attributes
.attributes()
.iter()
.filter_map(|a| {
if let Node::Attribute(a) = a {
if let NodeAttribute::Attribute(a) = a {
if a.key.to_string() == "style" {
if a.value.as_ref().and_then(value_to_string).is_some()
if a.value().and_then(value_to_string).is_some()
|| fancy_style_name(&a.key.to_string(), cx, a).is_some()
{
None
} else {
Some((a.key.span(), &a.value))
Some((a.key.span(), a.value()))
}
} else {
None
@@ -779,10 +786,10 @@ fn set_style_attribute_ssr(
.collect::<Vec<_>>();
let style_attrs = node
.attributes
.attributes()
.iter()
.filter_map(|node| {
if let Node::Attribute(node) = node {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if name == "style" {
return if let Some((_, name, value)) =
@@ -825,7 +832,6 @@ fn set_style_attribute_ssr(
for (_span, value) in dyn_style_attr {
if let Some(value) = value {
template.push_str(" {};");
let value = value.as_ref();
holes.push(quote! {
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
@@ -899,18 +905,18 @@ fn fragment_to_tokens(
let tokens = if lazy {
quote! {
{
leptos::Fragment::lazy(|| vec![
leptos::Fragment::lazy(|| [
#(#nodes),*
])
].to_vec())
#view_marker
}
}
} else {
quote! {
{
leptos::Fragment::new(vec![
leptos::Fragment::new([
#(#nodes),*
])
].to_vec())
#view_marker
}
}
@@ -948,18 +954,14 @@ fn node_to_tokens(
view_marker,
),
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
Node::Text(node) => {
let value = node.value.as_ref();
Some(quote! {
leptos::leptos_dom::html::text(#value)
})
}
Node::Block(node) => {
let value = node.value.as_ref();
Some(quote! { #value })
}
Node::Attribute(node) => {
Some(attribute_to_tokens(cx, node, global_class))
Node::Text(node) => Some(quote! {
leptos::leptos_dom::html::text(#node)
}),
Node::Block(node) => Some(quote! { #node }),
Node::RawText(r) => {
let text = r.to_string_best();
let text = syn::LitStr::new(&text, r.span());
Some(quote! { #text })
}
Node::Element(node) => element_to_tokens(
cx,
@@ -980,6 +982,7 @@ fn element_to_tokens(
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> Option<TokenStream> {
let name = node.name();
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
slot_to_tokens(cx, node, slot, parent_slots, global_class);
@@ -988,20 +991,17 @@ fn element_to_tokens(
Some(component_to_tokens(cx, node, global_class))
}
} else {
let tag = node.name.to_string();
let tag = name.to_string();
let name = if is_custom_element(&tag) {
let name = node.name.to_string();
let name = node.name().to_string();
quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) }
} else if is_svg_element(&tag) {
let name = &node.name;
parent_type = TagType::Svg;
quote! { leptos::leptos_dom::svg::#name(#cx) }
} else if is_math_ml_element(&tag) {
let name = &node.name;
parent_type = TagType::Math;
quote! { leptos::leptos_dom::math::#name(#cx) }
} else if is_ambiguous_element(&tag) {
let name = &node.name;
match parent_type {
TagType::Unknown => {
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
@@ -1020,12 +1020,11 @@ fn element_to_tokens(
}
}
} else {
let name = &node.name;
parent_type = TagType::Html;
quote! { leptos::leptos_dom::html::#name(#cx) }
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
let name = name.trim();
if name.starts_with("class:")
@@ -1041,8 +1040,8 @@ fn element_to_tokens(
None
}
});
let class_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let class_attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
Some(fancy)
@@ -1055,8 +1054,8 @@ fn element_to_tokens(
None
}
});
let style_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let style_attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) {
Some(fancy)
@@ -1101,32 +1100,18 @@ fn element_to_tokens(
}),
false,
),
Node::Text(node) => {
if let Some(primitive) = value_to_string(&node.value) {
(quote! { #primitive }, true)
} else {
let value = node.value.as_ref();
(
quote! {
#[allow(unused_braces)] #value
},
false,
)
}
}
Node::Block(node) => {
if let Some(primitive) = value_to_string(&node.value) {
(quote! { #primitive }, true)
} else {
let value = node.value.as_ref();
(
quote! {
#[allow(unused_braces)] #value
},
false,
)
}
Node::Text(node) => (quote! { #node }, true),
Node::RawText(node) => {
let text = node.to_string_best();
let text = syn::LitStr::new(&text, node.span());
(quote! { #text }, true)
}
Node::Block(node) => (
quote! {
#node
},
false,
),
Node::Element(node) => (
element_to_tokens(
cx,
@@ -1139,9 +1124,7 @@ fn element_to_tokens(
.unwrap_or_default(),
false,
),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
(quote! {}, false)
}
Node::Comment(_) | Node::Doctype(_) => (quote! {}, false),
};
if is_static {
quote! {
@@ -1172,7 +1155,7 @@ fn element_to_tokens(
fn attribute_to_tokens(
cx: &Ident,
node: &NodeAttribute,
node: &KeyedAttribute,
global_class: Option<&TokenTree>,
) -> TokenStream {
let span = node.key.span();
@@ -1303,7 +1286,7 @@ fn attribute_to_tokens(
// special case of global_class and class attribute
if name == "class"
&& global_class.is_some()
&& node.value.as_ref().and_then(value_to_string).is_none()
&& node.value().and_then(value_to_string).is_none()
{
let span = node.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \
@@ -1313,10 +1296,8 @@ fn attribute_to_tokens(
};
// all other attributes
let value = match node.value.as_ref() {
let value = match node.value() {
Some(value) => {
let value = value.as_ref();
quote! { #value }
}
None => quote_spanned! { span => "" },
@@ -1367,7 +1348,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
pub(crate) fn slot_to_tokens(
cx: &Ident,
node: &NodeElement,
slot: &NodeAttribute,
slot: &KeyedAttribute,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
) {
@@ -1376,19 +1357,19 @@ pub(crate) fn slot_to_tokens(
let name = convert_to_snake_case(if name.starts_with("slot:") {
name.replacen("slot:", "", 1)
} else {
node.name.to_string()
node.name().to_string()
});
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
let component_name = ident_from_tag_name(node.name());
let span = node.name().span();
let Some(parent_slots) = parent_slots else {
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
return;
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
if is_slot(node) {
None
} else {
@@ -1406,10 +1387,8 @@ pub(crate) fn slot_to_tokens(
let name = &attr.key;
let value = attr
.value
.as_ref()
.value()
.map(|v| {
let v = v.as_ref();
quote! { #v }
})
.unwrap_or_else(|| quote! { #name });
@@ -1474,9 +1453,9 @@ pub(crate) fn slot_to_tokens(
let slot = Ident::new(&slot, span);
if values.len() > 1 {
quote! {
.#slot(vec![
.#slot([
#(#values)*
])
].to_vec())
}
} else {
let value = &values[0];
@@ -1504,12 +1483,12 @@ pub(crate) fn component_to_tokens(
node: &NodeElement,
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = &node.name;
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
let name = node.name();
let component_name = ident_from_tag_name(node.name());
let span = node.name().span();
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
let attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
Some(node)
} else {
None
@@ -1526,10 +1505,8 @@ pub(crate) fn component_to_tokens(
let name = &attr.key;
let value = attr
.value
.as_ref()
.value()
.map(|v| {
let v = v.as_ref();
quote! { #v }
})
.unwrap_or_else(|| quote! { #name });
@@ -1637,7 +1614,7 @@ pub(crate) fn component_to_tokens(
}
pub(crate) fn event_from_attribute_node(
attr: &NodeAttribute,
attr: &KeyedAttribute,
force_undelegated: bool,
) -> (TokenStream, &Expr) {
let event_name = attr
@@ -1697,7 +1674,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
match expr {
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
if let syn::Stmt::Expr(expr) = stmt {
if let syn::Stmt::Expr(expr, ..) = stmt {
expr_to_ident(expr)
} else {
None
@@ -1708,15 +1685,15 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
}
}
fn is_slot(node: &NodeAttribute) -> bool {
fn is_slot(node: &KeyedAttribute) -> bool {
let key = node.key.to_string();
let key = key.trim();
key == "slot" || key.starts_with("slot:")
}
fn get_slot(node: &NodeElement) -> Option<&NodeAttribute> {
node.attributes.iter().find_map(|node| {
if let Node::Attribute(node) = node {
fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
node.attributes().iter().find_map(|node| {
if let NodeAttribute::Attribute(node) = node {
if is_slot(node) {
Some(node)
} else {
@@ -1744,7 +1721,7 @@ fn is_self_closing(node: &NodeElement) -> bool {
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
matches!(
node.name.to_string().as_str(),
node.name().to_string().as_str(),
"area"
| "base"
| "br"
@@ -1899,13 +1876,13 @@ fn parse_event(event_name: &str) -> (&str, bool) {
fn fancy_class_name<'a>(
name: &str,
cx: &Ident,
node: &'a NodeAttribute,
node: &'a KeyedAttribute,
) -> Option<(TokenStream, String, &'a Expr)> {
// special case for complex class names:
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
if name == "class" {
if let Some(expr) = node.value.as_ref() {
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
if let Some(expr) = node.value() {
if let syn::Expr::Tuple(tuple) = expr {
if tuple.elems.len() == 2 {
let span = node.key.span();
let class = quote_spanned! {
@@ -1948,12 +1925,12 @@ fn fancy_class_name<'a>(
fn fancy_style_name<'a>(
name: &str,
cx: &Ident,
node: &'a NodeAttribute,
node: &'a KeyedAttribute,
) -> Option<(TokenStream, String, &'a Expr)> {
// special case for complex dynamic style names:
if name == "style" {
if let Some(expr) = node.value.as_ref() {
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
if let Some(expr) = node.value() {
if let syn::Expr::Tuple(tuple) = expr {
if tuple.elems.len() == 2 {
let span = node.key.span();
let style = quote_spanned! {

View File

@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
47 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component.rs:56:22

View File

@@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=`
45 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component_absolute.rs:54:22

View File

@@ -110,7 +110,7 @@ pub use slice::*;
pub use spawn::*;
pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::SuspenseContext;
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
pub use trigger::*;
mod macros {

View File

@@ -5,8 +5,9 @@ use crate::{
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalGetUntracked,
SignalSet, SignalUpdate, SignalWith, SuspenseContext, WriteSignal,
use_context, GlobalSuspenseContext, Memo, ReadSignal, Scope, ScopeProperty,
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
WriteSignal,
};
use std::{
any::Any,
@@ -820,6 +821,7 @@ where
f: impl FnOnce(&T) -> U,
location: &'static Location<'static>,
) -> Option<U> {
let global_suspense_cx = use_context::<GlobalSuspenseContext>(cx);
let suspense_cx = use_context::<SuspenseContext>(cx);
let v = self
@@ -882,6 +884,22 @@ where
}
}
}
if let Some(g) = &global_suspense_cx {
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
{
let s = g.as_inner();
if !contexts.contains(s) {
contexts.insert(*s);
if !has_value {
s.increment(
serializable != ResourceSerialization::Local,
);
}
}
}
}
};
create_isomorphic_effect(cx, increment);
@@ -1005,6 +1023,7 @@ where
}
}
#[derive(Clone)]
pub(crate) enum AnyResource {
Unserializable(Rc<dyn UnserializableResource>),
Serializable(Rc<dyn SerializableResource>),

View File

@@ -743,7 +743,7 @@ impl Runtime {
S: 'static,
T: 'static,
{
let resources = self.resources.borrow();
let resources = { self.resources.borrow().clone() };
let res = resources.get(id);
if let Some(res) = res {
let res_state = match res {
@@ -796,7 +796,8 @@ impl Runtime {
cx: Scope,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
let resources = { self.resources.borrow().clone() };
for (id, resource) in resources.iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(cx, id));
}

View File

@@ -2,11 +2,12 @@
#![forbid(unsafe_code)]
use crate::{
create_rw_signal, create_signal, queue_microtask, store_value, ReadSignal,
RwSignal, Scope, SignalUpdate, StoredValue, WriteSignal,
create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask,
signal::SignalGet, store_value, ReadSignal, RwSignal, Scope, SignalSet,
SignalUpdate, StoredValue, WriteSignal,
};
use futures::Future;
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
use std::{borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin};
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
@@ -20,6 +21,24 @@ pub struct SuspenseContext {
pub(crate) should_block: StoredValue<bool>,
}
/// A single, global suspense context that will be checked when resources
/// are read. This wont be “blocked” by lower suspense components. This is
/// useful for e.g., holding route transitions.
#[derive(Copy, Clone, Debug)]
pub struct GlobalSuspenseContext(SuspenseContext);
impl GlobalSuspenseContext {
/// Creates an empty global suspense context.
pub fn new(cx: Scope) -> Self {
Self(SuspenseContext::new(cx))
}
/// Returns a reference to the underlying suspense context.
pub fn as_inner(&self) -> &SuspenseContext {
&self.0
}
}
impl SuspenseContext {
/// Whether the suspense contains local resources at this moment,
/// and therefore can't be serialized
@@ -32,6 +51,25 @@ impl SuspenseContext {
pub fn should_block(&self) -> bool {
self.should_block.get_value()
}
/// Returns a `Future` that resolves when this suspense is resolved.
pub fn to_future(&self, cx: Scope) -> impl Future<Output = ()> {
use futures::StreamExt;
let pending_resources = self.pending_resources;
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let tx = RefCell::new(tx);
queue_microtask(move || {
create_isomorphic_effect(cx, move |_| {
if pending_resources.get() == 0 {
_ = tx.borrow_mut().try_send(());
}
})
});
async move {
rx.next().await;
}
}
}
impl std::hash::Hash for SuspenseContext {
@@ -98,6 +136,12 @@ impl SuspenseContext {
});
}
/// Resets the counter of pending resources.
pub fn clear(&self) {
self.set_pending_resources.set(0);
self.pending_serializable_resources.set(0);
}
/// Tests whether all of the pending resources have resolved.
pub fn ready(&self) -> bool {
self.pending_resources

View File

@@ -90,10 +90,14 @@ where
children: Children,
) -> HtmlElement<leptos::html::A> {
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
_ = state;
{
_ = state;
}
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
_ = replace;
{
_ = replace;
}
let location = use_location(cx);
let is_active = create_memo(cx, move |_| match href.get() {

View File

@@ -1,6 +1,7 @@
mod form;
mod link;
mod outlet;
mod progress;
mod redirect;
mod route;
mod router;
@@ -9,6 +10,7 @@ mod routes;
pub use form::*;
pub use link::*;
pub use outlet::*;
pub use progress::*;
pub use redirect::*;
pub use route::*;
pub use router::*;

View File

@@ -1,6 +1,6 @@
use crate::{
animation::{Animation, AnimationState},
use_is_back_navigation, use_route,
use_is_back_navigation, use_route, SetIsRouting,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{cell::Cell, rc::Rc};
@@ -45,6 +45,35 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
}
});
let outlet: Signal<Option<View>> =
if cfg!(any(feature = "csr", feature = "hydrate"))
&& use_context::<SetIsRouting>(cx).is_some()
{
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
let is_fallback =
create_memo(cx, move |_| !global_suspense.as_inner().ready());
let last_two_views = create_memo(
cx,
move |prev: Option<&(Option<View>, Option<View>)>| match prev {
None => (outlet.get(), None),
Some((curr, _)) => (outlet.get(), curr.clone()),
},
);
create_memo(cx, move |_| {
let (curr, prev) = last_two_views.get();
if is_fallback.get() && prev.is_some() {
prev
} else {
curr
}
})
.into()
} else {
outlet.into()
};
leptos::leptos_dom::DynChild::new_with_id(id, move || outlet.get())
}

View File

@@ -0,0 +1,66 @@
use leptos::{leptos_dom::helpers::IntervalHandle, *};
/// A visible indicator that the router is in the process of navigating
/// to another route.
///
/// This is used when `<Router set_is_routing>` has been provided, to
/// provide some visual indicator that the page is currently loading
/// async data, so that it is does not appear to have frozen. It can be
/// styled independently.
#[component]
pub fn RoutingProgress(
cx: Scope,
/// Whether the router is currently loading the new page.
#[prop(into)]
is_routing: Signal<bool>,
/// The maximum expected time for loading, which is used to
/// calibrate the animation process.
#[prop(optional, into)]
max_time: std::time::Duration,
/// The time to show the full progress bar after page has loaded, before hiding it. (Defaults to 100ms.)
#[prop(default = std::time::Duration::from_millis(250))]
before_hiding: std::time::Duration,
/// CSS classes to be applied to the `<progress>`.
#[prop(optional, into)]
class: String,
) -> impl IntoView {
const INCREMENT_EVERY_MS: f32 = 5.0;
let expected_increments =
max_time.as_secs_f32() / (INCREMENT_EVERY_MS / 1000.0);
let percent_per_increment = 100.0 / expected_increments;
let (is_showing, set_is_showing) = create_signal(cx, false);
let (progress, set_progress) = create_signal(cx, 0.0);
create_effect(cx, move |prev: Option<Option<IntervalHandle>>| {
if is_routing.get() {
set_is_showing.set(true);
set_interval_with_handle(
move || {
set_progress.update(|n| *n += percent_per_increment);
},
std::time::Duration::from_millis(INCREMENT_EVERY_MS as u64),
)
.ok()
} else {
set_progress.set(100.0);
set_timeout(
move || {
set_progress.set(0.0);
set_is_showing.set(false);
},
before_hiding,
);
if let Some(Some(interval)) = prev {
interval.clear();
}
None
}
});
view! { cx,
<Show when=move || is_showing.get() fallback=|_| ()>
<progress class=class.clone() min="0" max="100" value=move || progress.get()/>
</Show>
}
}

View File

@@ -24,6 +24,9 @@ pub fn Router(
/// A fallback that should be shown if no route is matched.
#[prop(optional)]
fallback: Option<fn(Scope) -> View>,
/// A signal that will be set while the navigation process is underway.
#[prop(optional, into)]
set_is_routing: Option<SignalSetter<bool>>,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
@@ -32,10 +35,17 @@ pub fn Router(
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, base, fallback);
provide_context(cx, router);
provide_context(cx, GlobalSuspenseContext::new(cx));
if let Some(set_is_routing) = set_is_routing {
provide_context(cx, SetIsRouting(set_is_routing));
}
children(cx)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct SetIsRouting(pub SignalSetter<bool>);
/// Context type that contains information about the current router state.
#[derive(Debug, Clone)]
pub struct RouterContext {
@@ -228,6 +238,11 @@ impl RouterContextInner {
resolve_path("", to, None).map(String::from)
};
// reset count of pending resources at global level
expect_context::<GlobalSuspenseContext>(cx)
.as_inner()
.clear();
match resolved_to {
None => Err(NavigationError::NotRoutable(to.to_string())),
Some(resolved_to) => {
@@ -262,18 +277,32 @@ impl RouterContextInner {
move |state| *state = next_state
});
self.path_stack.update_value(|stack| {
let global_suspense =
expect_context::<GlobalSuspenseContext>(cx);
let path_stack = self.path_stack;
path_stack.update_value(|stack| {
stack.push(resolved_to.clone())
});
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to,
replace: false,
scroll: true,
state,
})
let set_is_routing = use_context::<SetIsRouting>(cx);
if let Some(set_is_routing) = set_is_routing {
set_is_routing.0.set(true);
}
spawn_local(async move {
if let Some(set_is_routing) = set_is_routing {
global_suspense.as_inner().to_future(cx).await;
set_is_routing.0.set(false);
}
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to,
replace: false,
scroll: true,
state,
});
}
});
}
Ok(())

View File

@@ -4,7 +4,7 @@ use crate::{
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
RouteDefinition, RouteMatch,
},
use_is_back_navigation, RouteContext, RouterContext,
use_is_back_navigation, RouteContext, RouterContext, SetIsRouting,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
@@ -56,6 +56,7 @@ pub fn Routes(
let id = HydrationCtx::id();
let root = root_route(cx, base_route, route_states, root_equal);
leptos::leptos_dom::DynChild::new_with_id(id, move || root.get())
.into_view(cx)
}
@@ -408,37 +409,67 @@ fn root_route(
) -> Memo<Option<View>> {
let root_cx = RefCell::new(None);
create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
if state.routes.borrow().is_empty() {
Some(base_route.outlet(cx).into_view(cx))
} else {
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
if prev.is_none() || !root_equal.get() {
let (root_view, _) = cx.run_child_scope(|cx| {
let prev_cx = std::mem::replace(
&mut *root_cx.borrow_mut(),
Some(cx),
);
if let Some(prev_cx) = prev_cx {
prev_cx.dispose();
}
root.as_ref()
.map(|route| route.outlet(cx).into_view(cx))
});
root_view
let root_view = create_memo(cx, {
let root_equal = Rc::clone(&root_equal);
move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
if state.routes.borrow().is_empty() {
Some(base_route.outlet(cx).into_view(cx))
} else {
prev.cloned().unwrap()
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
if prev.is_none() || !root_equal.get() {
let (root_view, _) = cx.run_child_scope(|cx| {
let prev_cx = std::mem::replace(
&mut *root_cx.borrow_mut(),
Some(cx),
);
if let Some(prev_cx) = prev_cx {
prev_cx.dispose();
}
root.as_ref()
.map(|route| route.outlet(cx).into_view(cx))
});
root_view
} else {
prev.cloned().unwrap()
}
}
})
}
});
if cfg!(any(feature = "csr", feature = "hydrate"))
&& use_context::<SetIsRouting>(cx).is_some()
{
let global_suspense = expect_context::<GlobalSuspenseContext>(cx);
let is_fallback =
create_memo(cx, move |_| !global_suspense.as_inner().ready());
let last_two_views = create_memo(
cx,
move |prev: Option<&(Option<View>, Option<View>)>| match prev {
None => (root_view.get(), None),
Some((curr, _)) => (root_view.get(), curr.clone()),
},
);
create_memo(cx, move |_| {
let (curr, prev) = last_two_views.get();
if is_fallback.get() && prev.is_some() && !root_equal.get() {
prev
} else {
curr
}
})
})
} else {
root_view
}
}
#[derive(Clone, Debug, PartialEq)]

View File

@@ -42,7 +42,11 @@ impl TryFrom<&str> for Url {
Ok(Self {
origin: url.origin(),
pathname: url.pathname(),
search: url.search(),
search: url
.search()
.strip_prefix('?')
.map(String::from)
.unwrap_or_default(),
search_params: ParamsMap(
try_iter(&url.search_params())
.map_js_error()?

View File

@@ -15,7 +15,7 @@ serde_qs = "0.12"
thiserror = "1"
serde_json = "1"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
proc-macro2 = "1"
ciborium = "0.2"
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }

View File

@@ -11,7 +11,7 @@ description = "The default implementation of the server_fn macro without a conte
proc-macro = true
[dependencies]
syn = { version = "1", features = ["full"] }
syn = { version = "2", features = ["full"] }
server_fn_macro = { workspace = true }
[dev-dependencies]

View File

@@ -11,7 +11,7 @@ readme = "../README.md"
[dependencies]
serde = { version = "1", features = ["derive"] }
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
syn = { version = "2", features = ["full", "parsing", "extra-traits"] }
proc-macro2 = "1"
proc-macro-error = "1"
xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }

View File

@@ -75,9 +75,11 @@ pub fn server_macro_impl(
struct_name,
prefix,
encoding,
fn_path,
..
} = syn::parse2::<ServerFnName>(args)?;
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
let encoding = quote!(#server_fn_path::#encoding);
let body = syn::parse::<ServerFnBody>(body.into())?;
@@ -213,7 +215,11 @@ pub fn server_macro_impl(
}
fn url() -> &'static str {
if !#fn_path.is_empty(){
#fn_path
} else {
#server_fn_path::const_format::concatcp!(#fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64(concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), 0))
}
}
fn encoding() -> #server_fn_path::Encoding {
@@ -260,6 +266,8 @@ struct ServerFnName {
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Path,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl Parse for ServerFnName {
@@ -280,6 +288,8 @@ impl Parse for ServerFnName {
}
})
.unwrap_or_else(|_| syn::parse_quote!(Encoding::Url));
let _comma3 = input.parse()?;
let fn_path = input.parse()?;
Ok(Self {
struct_name,
@@ -287,6 +297,8 @@ impl Parse for ServerFnName {
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
}
}