Erased mode in CI (#3640)

* Erased mode in CI

* Trigger CI

* Rename dev_mode erased_mode plus add to more matrices

* nested routes in separate component fix

* Fix lint

* Small fixes

* Fixes

* proc-macro rustflags cross-compilation workaround with internal erasure feature for leptos_macro

* Re-trigger CI

* fix unrelated doc CI and remove unneeded IntoAttribute trait

* Fix StaticVec rebuild() fn

* Conflict fixes

* Maybe fix

* Bump example toolchain
This commit is contained in:
zakstucke
2025-03-01 15:43:13 +00:00
committed by GitHub
parent cdee2a9476
commit 98e00fcb3b
25 changed files with 336 additions and 258 deletions

View File

@@ -28,5 +28,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -25,5 +25,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: stable

View File

@@ -25,5 +25,6 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
erased_mode: ${{ matrix.erased_mode }}
cargo_make_task: "ci"
toolchain: nightly-2025-02-19

View File

@@ -50,5 +50,5 @@ jobs:
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
fi

View File

@@ -28,7 +28,7 @@ jobs:
sed 's/\/$//' |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -24,7 +24,7 @@ jobs:
sed "s|$(pwd)/||" |
jq -R -s -c 'split("\n")[:-1]')
echo "Leptos Directories: $crates"
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
- name: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"

View File

@@ -5,6 +5,9 @@ on:
directory:
required: true
type: string
erased_mode:
required: true
type: boolean
cargo_make_task:
required: true
type: string
@@ -15,9 +18,10 @@ env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
runs-on: ubuntu-latest
steps:
- name: Free Disk Space

View File

@@ -85,7 +85,7 @@ pub fn fetch_example() -> impl IntoView {
.map(|s| {
view! {
<li>
<img src=s.clone()/>
<img src=s.clone() />
</li>
}
})

View File

@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
</div>
<div class="col-md-6">
<div class="row">
<Button id="run" text="Create 1,000 rows" on:click=run/>
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
<Button id="add" text="Append 1,000 rows" on:click=add/>
<Button id="update" text="Update every 10th row" on:click=update/>
<Button id="clear" text="Clear" on:click=clear/>
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
<Button id="run" text="Create 1,000 rows" on:click=run />
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
<Button id="add" text="Append 1,000 rows" on:click=add />
<Button id="update" text="Update every 10th row" on:click=update />
<Button id="clear" text="Clear" on:click=clear />
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
</div>
</div>
</div>

View File

@@ -9,7 +9,6 @@ use leptos_router::{
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
@@ -33,7 +32,7 @@ pub fn RouterExample() -> impl IntoView {
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
@@ -53,15 +52,15 @@ pub fn RouterExample() -> impl IntoView {
<Routes transition=true fallback=|| "This page could not be found.">
// paths can be created using the path!() macro, or provided as types like
// StaticSegment("about")
<Route path=path!("about") view=About/>
<Route path=path!("about") view=About />
<ProtectedRoute
path=path!("settings")
condition=move || Some(logged_in.get())
redirect_path=|| "/"
view=Settings
/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
<ContactRoutes/>
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
<ContactRoutes />
</Routes>
</main>
</Router>
@@ -71,11 +70,11 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>
<Route path=path!("/") view=|| "Select a contact."/>
<Route path=path!("/:id") view=Contact/>
<Route path=path!("/") view=|| "Select a contact." />
<Route path=path!("/:id") view=Contact />
</ParentRoute>
}
.into_inner()
@@ -122,7 +121,7 @@ pub fn ContactList() -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
<ul>{contacts}</ul>
</Suspense>
<Outlet/>
<Outlet />
</div>
}
}
@@ -166,7 +165,7 @@ pub fn Contact() -> impl IntoView {
Some(contact) => Either::Right(view! {
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1} <br/> {contact.address_2}</p>
<p>{contact.address_1} <br /> {contact.address_2}</p>
</section>
}),
}
@@ -224,10 +223,10 @@ pub fn Settings() -> impl IntoView {
<Form action="">
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="last_name" placeholder="Last"/>
<input type="text" name="first_name" placeholder="First" />
<input type="text" name="last_name" placeholder="Last" />
</fieldset>
<input type="submit"/>
<input type="submit" />
<p>
"This uses the " <code>"<Form/>"</code>
" component, which enhances forms by using client-side navigation for "

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "nightly-2024-01-29"
channel = "nightly-2025-02-19"

View File

@@ -4,7 +4,7 @@ use leptos_router::{
hooks::use_params,
nested_router::Outlet,
params::Params,
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
};
#[cfg(feature = "ssr")]
@@ -203,20 +203,20 @@ pub struct SuspenseCounters {
}
#[component]
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
// TODO should make this mode configurable via feature flag?
let ssr = SsrMode::Async;
view! {
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
<Route path=StaticSegment("/") view=InstrumentedTop/>
<Route path=StaticSegment("/") view=InstrumentedTop />
<ParentRoute path=StaticSegment("item") view=ItemRoot>
<Route path=StaticSegment("/") view=ItemListing/>
<Route path=StaticSegment("/") view=ItemListing />
<ParentRoute path=ParamSegment("id") view=ItemTop>
<Route path=StaticSegment("/") view=ItemOverview/>
<Route path=WildcardSegment("path") view=ItemInspect/>
<Route path=StaticSegment("/") view=ItemOverview />
<Route path=WildcardSegment("path") view=ItemInspect />
</ParentRoute>
</ParentRoute>
<Route path=StaticSegment("counters") view=ShowCounters/>
<Route path=StaticSegment("counters") view=ShowCounters />
</ParentRoute>
}
.into_inner()
@@ -279,32 +279,41 @@ fn InstrumentedRoot() -> impl IntoView {
<section id="instrumented">
<nav>
<a href="/">"Site Root"</a>
<A href="./" exact=true>"Instrumented Root"</A>
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
<A href="counters" strict_trailing_slash=true>"Counters"</A>
<A href="./" exact=true>
"Instrumented Root"
</A>
<A href="item/" strict_trailing_slash=true>
"Item Listing"
</A>
<A href="counters" strict_trailing_slash=true>
"Counters"
</A>
</nav>
<FieldNavPortlet/>
<Outlet/>
<Suspense>{
move || Suspend::new(async move {
<FieldNavPortlet />
<Outlet />
<Suspense>
{move || Suspend::new(async move {
let clear_suspense_counters = move |_| {
counters.update(|c| *c = SuspenseCounters::default());
};
csr_ticket.get().map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters/>
</ActionForm>
}
})
})
}</Suspense>
csr_ticket
.get()
.map(|ticket| {
let ticket = ticket.0;
view! {
<ActionForm action=reset_counters>
<input type="hidden" name="ticket" value=format!("{ticket}") />
<input
id="reset-csr-counters"
type="submit"
value="Reset CSR Counters"
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
})}
</Suspense>
<footer>
<nav>
<A href="item/3/">"Target 3##"</A>
@@ -323,11 +332,17 @@ fn InstrumentedRoot() -> impl IntoView {
fn InstrumentedTop() -> impl IntoView {
view! {
<h1>"Instrumented Tests"</h1>
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
<p>
"These tests validates the number of invocations of server functions and suspenses per access."
</p>
<ul>
// not using `A` because currently some bugs with artix
<li><a href="item/">"Item Listing"</a></li>
<li><a href="item/4/path1/">"Target 41#"</a></li>
<li>
<a href="item/">"Item Listing"</a>
</li>
<li>
<a href="item/4/path1/">"Target 41#"</a>
</li>
</ul>
}
}
@@ -342,7 +357,7 @@ fn ItemRoot() -> impl IntoView {
view! {
<h2>"<ItemRoot/>"</h2>
<Outlet/>
<Outlet />
}
}
@@ -360,7 +375,9 @@ fn ItemListing() -> impl IntoView {
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
view! {
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
<li>
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
</li>
}
)
.collect_view()
@@ -373,9 +390,7 @@ fn ItemListing() -> impl IntoView {
view! {
<h3>"<ItemListing/>"</h3>
<ul>
<Suspense>
{item_listing}
</Suspense>
<Suspense>{item_listing}</Suspense>
</ul>
}
}
@@ -402,7 +417,7 @@ fn ItemTop() -> impl IntoView {
));
view! {
<h4>"<ItemTop/>"</h4>
<Outlet/>
<Outlet />
}
}
@@ -412,24 +427,29 @@ fn ItemOverview() -> impl IntoView {
let resource = expect_context::<Resource<Option<GetItemResult>>>();
let item_view = move || {
Suspend::new(async move {
let result = resource.await.map(|GetItemResult(item, names)| view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>{
names.into_iter()
.map(|name| {
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
let id = item.id;
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/")>
"Inspect "{name.clone()}
</a></li>
}
})
.collect_view()
}</ul>
});
let result = resource.await.map(|GetItemResult(item, names)| {
view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>
{names
.into_iter()
.map(|name| {
let id = item.id;
// FIXME seems like relative link isn't working, it is currently
// adding an extra `/` in artix; manually construct `a` instead.
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
view! {
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/",
)>"Inspect "{name.clone()}</a>
</li>
}
})
.collect_view()}
</ul>
}
});
suspense_counters.update_untracked(|c| c.item_overview += 1);
result
})
@@ -437,9 +457,7 @@ fn ItemOverview() -> impl IntoView {
view! {
<h5>"<ItemOverview/>"</h5>
<Suspense>
{item_view}
</Suspense>
<Suspense>{item_view}</Suspense>
}
}
@@ -496,23 +514,26 @@ fn ItemInspect() -> impl IntoView {
));
view! {
<p>{format!("Inspecting {item:?}")}</p>
<ul>{
fields.iter()
<ul>
{fields
.iter()
.map(|field| {
// FIXME seems like relative link to root for a wildcard isn't
// working as expected, so manually construct `a` instead.
// let text = format!("Inspect {name}/{field}");
// view! {
// <li><A href=format!("{field}")>{text}</A></li>
// <li><A href=format!("{field}")>{text}</A></li>
// }
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
format!("Inspect {name}/{field}")
}</a></li>
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/{field}",
)>{format!("Inspect {name}/{field}")}</a>
</li>
}
})
.collect_view()
}</ul>
.collect_view()}
</ul>
}
});
suspense_counters.update_untracked(|c| c.item_inspect += 1);
@@ -527,9 +548,7 @@ fn ItemInspect() -> impl IntoView {
view! {
<h5>"<ItemInspect/>"</h5>
<Suspense>
{inspect_view}
</Suspense>
<Suspense>{inspect_view}</Suspense>
}
}
@@ -590,7 +609,8 @@ fn ShowCounters() -> impl IntoView {
id="reset-counters"
type="submit"
value="Reset Counters"
on:click=clear_suspense_counters/>
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
@@ -601,20 +621,23 @@ fn ShowCounters() -> impl IntoView {
<h2>"Counters"</h2>
<h3 id="suspend-calls">"Suspend Calls"</h3>
{move || suspense_counters.with(|c| view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
})}
{move || {
suspense_counters
.with(|c| {
view! {
<dl>
<dt>"item_listing"</dt>
<dd id="item_listing">{c.item_listing}</dd>
<dt>"item_overview"</dt>
<dd id="item_overview">{c.item_overview}</dd>
<dt>"item_inspect"</dt>
<dd id="item_inspect">{c.item_inspect}</dd>
</dl>
}
})
}}
<Suspense>
{counter_view}
</Suspense>
<Suspense>{counter_view}</Suspense>
}
}
@@ -642,17 +665,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
view! {
<div id="FieldNavPortlet">
<span>"FieldNavPortlet:"</span>
<nav>{
ctx.0.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! {
<A href=href>{text}</A>
}
})
.collect_view()
})
}</nav>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
</div>
}
})

View File

@@ -100,6 +100,15 @@ trace-component-props = [
]
delegation = ["tachys/delegation"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
[target.'cfg(erase_components)'.dependencies]
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
[package.metadata.cargo-all-features]
denylist = [
"nightly",

View File

@@ -51,6 +51,13 @@ trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
generic = ["server_fn_macro/generic"]
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
__internal_erase_components = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
@@ -83,9 +90,3 @@ skip_feature_sets = [
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -32,6 +32,8 @@ pub struct Model {
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemFn::parse(input)?;
maybe_modify_return_type(&mut item.sig.output);
convert_impl_trait_to_generic(&mut item.sig);
let docs = Docs::new(&item.attrs);
@@ -76,6 +78,39 @@ impl Parse for Model {
}
}
/// Exists to fix nested routes defined in a separate component in erased mode,
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
fn maybe_modify_return_type(ret: &mut ReturnType) {
#[cfg(feature = "__internal_erase_components")]
{
if let ReturnType::Type(_, ty) = ret {
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
if bounds.iter().any(|bound| {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
if trait_bound.path.segments.iter().any(
|path_segment| {
path_segment.ident == "MatchNestedRoutes"
},
) {
return true;
}
}
false
}) {
*ty = parse_quote!(
::leptos_router::any_nested_route::AnyNestedRoute
);
}
}
}
}
#[cfg(not(feature = "__internal_erase_components"))]
{
let _ = ret;
}
}
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
pub fn drain_filter<T>(
@@ -296,9 +331,9 @@ impl ToTokens for Model {
let component = if *is_transparent {
body_expr
} else if cfg!(erase_components) {
} else if cfg!(feature = "__internal_erase_components") {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
@@ -613,7 +648,8 @@ impl Parse for DummyModel {
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
let mut sig: Signature = input.parse()?;
maybe_modify_return_type(&mut sig.output);
// The body is left untouched, so it will not cause an error
// even if the syntax is invalid.

View File

@@ -281,7 +281,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn template(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, true)
if cfg!(feature = "__internal_erase_components") {
view(tokens)
} else {
view_macro_impl(tokens, true)
}
}
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {

View File

@@ -170,13 +170,13 @@ pub(crate) fn component_to_tokens(
.collect::<Vec<_>>();
let spreads = (!(spreads.is_empty())).then(|| {
if cfg!(erase_components) {
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr(vec![#(#spreads.into_attr().into_any_attr(),)*])
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
}
} else {
quote! {
.add_any_attr((#(#spreads,)*).into_attr())
.add_any_attr((#(#spreads,)*))
}
}
});

View File

@@ -428,7 +428,7 @@ fn element_children_to_tokens(
{ #child }
)
})
} else if cfg!(erase_components) {
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
.child(
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
@@ -479,7 +479,7 @@ fn fragment_to_tokens(
None
} else if children.len() == 1 {
children.into_iter().next()
} else if cfg!(erase_components) {
} else if cfg!(feature = "__internal_erase_components") {
Some(quote! {
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
})
@@ -768,9 +768,9 @@ pub(crate) fn element_to_tokens(
}
}
if cfg!(erase_components) {
if cfg!(feature = "__internal_erase_components") {
Some(quote! {
vec![#(#attributes.into_attr().into_any_attr(),)*]
vec![#(#attributes.into_any_attr(),)*]
#(.add_any_attr(#additions))*
})
} else {

View File

@@ -83,7 +83,7 @@ pub(crate) fn slot_to_tokens(
let value = attr.value().map(|v| {
quote! { #v }
})?;
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
Some(quote! { (#name, #value) })
})
.collect::<Vec<_>>();

View File

@@ -1,4 +1,4 @@
#[cfg(not(erase_components))]
#[cfg(not(feature = "__internal_erase_components"))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -78,25 +78,6 @@ pub trait Attribute: NextAttribute + Send {
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
}
/// A type that can be converted into an attribute.
///
/// Used type-erasing attrs and tuples of attrs to [`Vec<AnyAttribute>`] as early as possible to prevent type explosion.
pub trait IntoAttribute {
/// The type of the attribute.
type Output: Attribute;
/// Converts this into an attribute.
fn into_attr(self) -> Self::Output;
}
impl<T: Attribute> IntoAttribute for T {
type Output = T;
fn into_attr(self) -> Self::Output {
self
}
}
/// Adds another attribute to this one, returning a new attribute.
///
/// This is typically achieved by creating or extending a tuple of attributes.
@@ -288,7 +269,6 @@ where
macro_rules! impl_attr_for_tuples {
($first:ident, $($ty:ident),* $(,)?) => {
#[cfg(not(erase_components))]
impl<$first, $($ty),*> Attribute for ($first, $($ty,)*)
where
$first: Attribute,
@@ -376,7 +356,6 @@ macro_rules! impl_attr_for_tuples {
}
}
#[cfg(not(erase_components))]
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
where
$first: Attribute,
@@ -394,38 +373,15 @@ macro_rules! impl_attr_for_tuples {
($first, $($ty,)* new_attr)
}
}
#[cfg(erase_components)]
impl<$first, $($ty),*> IntoAttribute for ($first, $($ty,)*)
where
$first: IntoAttribute,
$($ty: IntoAttribute),*,
{
type Output = Vec<$crate::html::attribute::any_attribute::AnyAttribute>;
fn into_attr(self) -> Self::Output {
use crate::html::attribute::any_attribute::IntoAnyAttribute;
#[allow(non_snake_case)]
let ($first, $($ty,)*) = self;
vec![
$first.into_attr().into_any_attr(),
$($ty.into_attr().into_any_attr(),)*
]
}
}
};
}
macro_rules! impl_attr_for_tuples_truncate_additional {
($first:ident, $($ty:ident),* $(,)?) => {
#[cfg(not(erase_components))]
impl<$first, $($ty),*> Attribute for ($first, $($ty,)*)
where
$first: Attribute,
$($ty: Attribute),*,
{
const MIN_LENGTH: usize = $first::MIN_LENGTH $(+ $ty::MIN_LENGTH)*;
@@ -509,7 +465,6 @@ macro_rules! impl_attr_for_tuples_truncate_additional {
}
}
#[cfg(not(erase_components))]
impl<$first, $($ty),*> NextAttribute for ($first, $($ty,)*)
where
$first: Attribute,
@@ -526,38 +481,9 @@ macro_rules! impl_attr_for_tuples_truncate_additional {
//($first, $($ty,)*)
}
}
#[cfg(erase_components)]
impl<$first, $($ty),*> IntoAttribute for ($first, $($ty,)*)
where
$first: IntoAttribute,
$($ty: IntoAttribute),*,
{
type Output = $crate::html::attribute::any_attribute::AnyAttribute;
fn into_attr(self) -> Self::Output {
todo!("adding more than 26 attributes is not supported");
//crate::html::attribute::any_attribute::IntoAnyAttribute::into_any_attr(self)
}
}
};
}
#[cfg(erase_components)]
impl<A> IntoAttribute for (A,)
where
A: IntoAttribute,
{
type Output = Vec<crate::html::attribute::any_attribute::AnyAttribute>;
fn into_attr(self) -> Self::Output {
use crate::html::attribute::any_attribute::IntoAnyAttribute;
vec![self.0.into_attr().into_any_attr()]
}
}
#[cfg(not(erase_components))]
impl<A> Attribute for (A,)
where
A: Attribute,
@@ -615,7 +541,6 @@ where
}
}
#[cfg(not(erase_components))]
impl<A> NextAttribute for (A,)
where
A: Attribute,

View File

@@ -1,9 +1,10 @@
#[cfg(any(debug_assertions, leptos_debuginfo))]
use crate::hydration::set_currently_hydrating;
#[cfg(erase_components)]
use crate::view::any_view::AnyView;
use crate::{
html::attribute::Attribute,
hydration::{failed_to_cast_element, Cursor},
prelude::*,
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
@@ -133,36 +134,101 @@ trait NextChildren {
}
#[cfg(erase_components)]
impl NextChildren for () {
fn next_children(
self,
child: AnyView,
) -> crate::view::iterators::StaticVec<AnyView> {
vec![child].into()
}
}
mod erased_tuples {
use super::*;
use crate::view::{any_view::IntoAny, iterators::StaticVec};
#[cfg(erase_components)]
impl<T: RenderHtml> NextChildren for (T,) {
fn next_children(
self,
child: AnyView,
) -> crate::view::iterators::StaticVec<AnyView> {
use crate::view::any_view::IntoAny;
vec![self.0.into_owned().into_any(), child].into()
impl NextChildren for StaticVec<AnyView> {
fn next_children(mut self, child: AnyView) -> StaticVec<AnyView> {
self.0.push(child);
self
}
}
}
#[cfg(erase_components)]
impl NextChildren for crate::view::iterators::StaticVec<AnyView> {
fn next_children(
mut self,
child: AnyView,
) -> crate::view::iterators::StaticVec<AnyView> {
self.0.push(child);
self
impl NextChildren for () {
fn next_children(self, child: AnyView) -> StaticVec<AnyView> {
vec![child].into()
}
}
impl<T: RenderHtml> NextChildren for (T,) {
fn next_children(self, child: AnyView) -> StaticVec<AnyView> {
vec![self.0.into_owned().into_any(), child].into()
}
}
macro_rules! impl_next_children_tuples {
($($ty:ident),*) => {
impl<$($ty: RenderHtml),*> NextChildren for ($($ty,)*)
{
fn next_children(
self, child: AnyView,
) -> StaticVec<AnyView> {
#[allow(non_snake_case)]
let ($($ty,)*) = self;
vec![$($ty.into_owned().into_any(),)* child].into()
}
}
};
}
impl_next_children_tuples!(AA, BB);
impl_next_children_tuples!(AA, BB, CC);
impl_next_children_tuples!(AA, BB, CC, DD);
impl_next_children_tuples!(AA, BB, CC, DD, EE);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG, HH);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG, HH, II);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG, HH, II, JJ);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK);
impl_next_children_tuples!(AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT, UU
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT, UU, VV
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT, UU, VV, WW
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT, UU, VV, WW, XX
);
impl_next_children_tuples!(
AA, BB, CC, DD, EE, FF, GG, HH, II, JJ, KK, LL, MM, NN, OO, PP, QQ, RR,
SS, TT, UU, VV, WW, XX, YY
);
}
impl<E, At, Ch> AddAnyAttr for HtmlElement<E, At, Ch>
@@ -510,10 +576,8 @@ where
/// Renders an [`Attribute`] (which can be one or more HTML attributes) into an HTML buffer.
pub fn attributes_to_html<At>(attr: At, buf: &mut String) -> String
where
At: IntoAttribute,
At: Attribute,
{
let attr = attr.into_attr();
// `class` and `style` are created first, and pushed later
// this is because they can be filled by a mixture of values that include
// either the whole value (`class="..."` or `style="..."`) and individual

View File

@@ -22,7 +22,7 @@ pub mod prelude {
OnAttribute, OnTargetAttribute, PropAttribute,
StyleAttribute,
},
IntoAttribute, IntoAttributeValue,
IntoAttributeValue,
},
directive::DirectiveAttribute,
element::{ElementChild, ElementExt, InnerHtmlAttribute},

View File

@@ -145,14 +145,11 @@ where
}
}
#[cfg(erase_components)]
use crate::html::attribute::any_attribute::{AnyAttribute, IntoAnyAttribute};
#[cfg(erase_components)]
impl<A, B> NextAttribute for Either<A, B>
where
B: IntoAnyAttribute,
A: IntoAnyAttribute,
B: crate::html::attribute::any_attribute::IntoAnyAttribute,
A: crate::html::attribute::any_attribute::IntoAnyAttribute,
{
type Output<NewAttr: Attribute> = Vec<AnyAttribute>;
@@ -160,6 +157,8 @@ where
self,
new_attr: NewAttr,
) -> Self::Output<NewAttr> {
use crate::html::attribute::any_attribute::IntoAnyAttribute;
vec![
match self {
Either::Left(left) => left.into_any_attr(),

View File

@@ -434,6 +434,7 @@ where
T: Mountable,
{
states: Vec<T>,
parent: Option<crate::renderer::types::Element>,
}
impl<T> Mountable for StaticVecState<T>
@@ -452,6 +453,7 @@ where
for state in self.states.iter_mut() {
state.mount(parent, marker);
}
self.parent = Some(parent.clone());
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
@@ -479,17 +481,25 @@ where
fn build(self) -> Self::State {
Self::State {
states: self.0.into_iter().map(T::build).collect(),
parent: None,
}
}
fn rebuild(self, state: &mut Self::State) {
let Self::State { states } = state;
let old = states;
// this is an unkeyed diff
self.0
.into_iter()
.zip(old.iter_mut())
.for_each(|(new, old)| T::rebuild(new, old));
let Self::State { states, .. } = state;
// StaticVec's in general shouldn't need to be reused, but rebuild() will still trigger e.g. if 2 routes have the same tree,
// this can cause problems if differing in lengths. Because we don't use marker nodes in StaticVec, we rebuild the entire vec remounting to the parent.
for state in states {
state.unmount();
}
let parent = state
.parent
.take()
.expect("parent should always be Some() on a StaticVec rebuild()");
*state = self.build();
state.mount(&parent, None);
}
}
@@ -593,7 +603,8 @@ where
.into_iter()
.map(|child| child.hydrate::<FROM_SERVER>(cursor, position))
.collect();
Self::State { states }
let parent = cursor.current().parent_element();
Self::State { states, parent }
}
fn into_owned(self) -> Self::Owned {