Merge remote-tracking branch 'origin' into wasm-splitting-support

This commit is contained in:
Greg Johnston
2025-07-10 12:15:45 -04:00
126 changed files with 3180 additions and 1217 deletions

View File

@@ -13,11 +13,11 @@ edition.workspace = true
proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
attribute-derive = { features = ["syn-full"] , workspace = true, default-features = true }
cfg-if = { workspace = true, default-features = true }
html-escape = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
prettyplease = "0.2.32"
prettyplease = { workspace = true, default-features = true }
proc-macro-error2 = { default-features = false , workspace = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
@@ -26,17 +26,17 @@ rstml = { workspace = true, default-features = true }
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = { workspace = true , default-features = true }
uuid = { version = "1.16", features = ["v4"] }
uuid = { features = ["v4"] , workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
log = "0.4.27"
log = { workspace = true, default-features = true }
typed-builder = { workspace = true, default-features = true }
trybuild = { workspace = true , default-features = true }
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.42"
insta = { workspace = true, default-features = true }
serde = { workspace = true, default-features = true }
[build-dependencies]

View File

@@ -409,6 +409,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// generate documentation for the component.
///
/// Heres how you would define and use a simple Leptos component which can accept custom properties for a name and age:
///
/// ```rust
/// # use leptos::prelude::*;
/// use std::time::Duration;
@@ -446,6 +447,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// Here are some important details about how Leptos components work within the framework:
///
/// * **The component function only runs once.** Your component function is not a “render” function
/// that re-runs whenever changes happen in the state. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its okay
@@ -458,7 +460,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// ```
/// # use leptos::prelude::*;
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent() -> impl IntoView {}
@@ -500,8 +501,10 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// ## Customizing Properties
///
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
/// customize the types that component property can receive. You can use the following attributes:
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
@@ -514,6 +517,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
/// specified as either `None` or `Some(T)`.
/// * `#[prop(default = <expr>)]`: Optional property that specifies a default value, which is used when the
/// property is not specified.
/// * `#[prop(name = "new_name")]`: Specifiy a different name for the property. Can be used to destructure
/// fields in component function parameters (see example below).
///
/// ```rust
/// # use leptos::prelude::*;
///
@@ -522,6 +530,8 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// #[prop(into)] name: String,
/// #[prop(optional)] optional_value: Option<i32>,
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
/// #[prop(default = 7)] optional_default: i32,
/// #[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
/// ) -> impl IntoView {
/// // whatever UI you need
/// }
@@ -530,16 +540,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// pub fn App() -> impl IntoView {
/// view! {
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// optional_default=42 // received as `42`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// // optional values can be omitted
/// />
/// }
/// }
///
/// pub struct UserInfo {
/// pub email: &'static str,
/// pub user_id: &'static str,
/// }
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]

View File

@@ -44,6 +44,8 @@ pub fn render_view(
view_marker: Option<String>,
disable_inert_html: bool,
) -> Option<TokenStream> {
let disable_inert_html = disable_inert_html || global_class.is_some();
let (base, should_add_view) = match nodes.len() {
0 => {
let span = Span::call_site();
@@ -112,9 +114,9 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
return false;
}
// also doesn't work if the top-level element is an SVG/MathML element
// also doesn't work if the top-level element is a MathML element
let el_name = el.name().to_string();
if is_svg_element(&el_name) || is_math_ml_element(&el_name) {
if is_math_ml_element(&el_name) {
return false;
}
}
@@ -300,7 +302,7 @@ fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
) -> TokenStream {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
@@ -396,9 +398,117 @@ fn inert_element_to_tokens(
html.finish();
Some(quote! {
quote! {
::leptos::tachys::html::InertElement::new(#html)
})
}
}
/// # Note
/// Should not be used on top level `<svg>` elements.
/// Use [`inert_element_to_tokens`] instead.
fn inert_svg_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> TokenStream {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
while let Some(current) = nodes.pop_front() {
match current {
Item::ClosingTag(tag) => {
// closing tag
html.push_str("</");
html.push_str(&tag);
html.push('>');
}
Item::Node(current, escape) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
// opening tag
html.push('<');
html.push_str(&el_name);
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
let attr_name = attr.key.to_string();
// trim r# from raw identifiers like r#as
let attr_name =
attr_name.trim_start_matches("r#");
if attr_name != "class" {
html.push(' ');
html.push_str(attr_name);
}
if let Some(value) =
attr.possible_value.to_value()
{
if let KVAttributeValue::Expr(Expr::Lit(
lit,
)) = &value.value
{
if let Lit::Str(txt) = &lit.lit {
let value = txt.value();
let value = html_escape::encode_double_quoted_attribute(&value);
if attr_name == "class" {
html.push_class(&value);
} else {
html.push_str("=\"");
html.push_str(&value);
html.push('"');
}
}
}
};
}
}
html.push('>');
// render all children
if !self_closing {
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child, escape));
}
}
}
_ => {}
}
}
}
}
html.finish();
quote! {
::leptos::tachys::svg::InertElement::new(#html)
}
}
fn element_children_to_tokens(
@@ -431,7 +541,9 @@ fn element_children_to_tokens(
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
),*])
)
})
} else if children.len() > 16 {
@@ -481,7 +593,9 @@ fn fragment_to_tokens(
children.into_iter().next()
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
::leptos::tachys::view::iterators::StaticVec::from(vec![#(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(#children)
),*])
})
} else if children.len() > 16 {
// implementations of various traits used in routing and rendering are implemented for
@@ -593,7 +707,17 @@ fn node_to_tokens(
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
inert_element_to_tokens(node, escape, global_class)
let el_name = el_node.name().to_string();
if is_svg_element(&el_name) && el_name != "svg" {
Some(inert_svg_element_to_tokens(
node,
escape,
global_class,
))
} else {
Some(inert_element_to_tokens(node, escape, global_class))
}
} else {
element_to_tokens(
el_node,

View File

@@ -119,3 +119,45 @@ fn returns_static_lifetime() {
WithLifetime(WithLifetimeProps::builder().data(&val).build())
}
}
// an attempt to catch unhygienic macros regression
mod macro_hygiene {
// To ensure no relative module path to leptos inside macros.
mod leptos {}
// doing this separately to below due to this being the smallest
// unit with the lowest import surface.
#[test]
fn view() {
use ::leptos::IntoView;
use ::leptos_macro::{component, view};
#[component]
fn Component() -> impl IntoView {
view! {
{()}
{()}
}
}
}
// may extend this test with other items as necessary.
#[test]
fn view_into_any() {
use ::leptos::{
prelude::{ElementChild, IntoAny},
IntoView,
};
use ::leptos_macro::{component, view};
#[component]
fn Component() -> impl IntoView {
view! {
<div>
{().into_any()}
{()}
</div>
}
}
}
}