diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index dae1951e1..48fe89472 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -5,9 +5,9 @@ use nest::{Nest, NestId}; use proc_macro::TokenStream; use quote::{__private::Span, format_ident, quote, ToTokens}; use redirect::Redirect; -use route::Route; +use route::{Route, RouteType}; use segment::RouteSegment; -use syn::{parse::ParseStream, parse_macro_input, Ident, Token}; +use syn::{parse::ParseStream, parse_macro_input, Ident, Token, Type}; use proc_macro2::TokenStream as TokenStream2; @@ -73,11 +73,13 @@ mod segment; /// #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})] /// #[route("/:dynamic")] /// Route3 { dynamic: String }, +/// #[child] +/// NestedRoute(NestedRoute), /// } /// ``` #[proc_macro_derive( Routable, - attributes(route, nest, end_nest, layout, end_layout, redirect) + attributes(route, nest, end_nest, layout, end_layout, redirect, child) )] pub fn routable(input: TokenStream) -> TokenStream { let routes_enum = parse_macro_input!(input as syn::ItemEnum); @@ -124,12 +126,12 @@ pub fn routable(input: TokenStream) -> TokenStream { } #error_type + + #display_impl + + #routable_impl #parse_impl - - #display_impl - - #routable_impl } .into() } @@ -288,7 +290,33 @@ impl RouteEnum { let route = Route::parse(active_nests, active_layouts, variant.clone())?; // add the route to the site map - if let Some(segment) = SiteMapSegment::new(&route.segments) { + let mut segment = SiteMapSegment::new(&route.segments); + if let RouteType::Child(child) = &route.ty { + let new_segment = SiteMapSegment { + segment_type: SegmentType::Child(child.clone()), + children: Vec::new(), + }; + match &mut segment { + Some(segment) => { + fn set_last_child_to( + segment: &mut SiteMapSegment, + new_segment: SiteMapSegment, + ) { + if let Some(last) = segment.children.last_mut() { + set_last_child_to(last, new_segment); + } else { + segment.children = vec![new_segment]; + } + } + set_last_child_to(segment, new_segment); + } + None => { + segment = Some(new_segment); + } + } + } + + if let Some(segment) = segment { let parent = site_map_stack.last_mut(); let children = match parent { Some(parent) => &mut parent.last_mut().unwrap().children, @@ -443,31 +471,13 @@ impl RouteEnum { let name = &self.name; let site_map = &self.site_map; - let mut layers = Vec::new(); + let mut matches = Vec::new(); - loop { - let index = layers.len(); - let mut routable_match = Vec::new(); - - // Collect all routes that match the current layer - for route in &self.routes { - if let Some(matched) = route.routable_match(&self.layouts, &self.nests, index) { - routable_match.push(matched); - } - } - - // All routes are exhausted - if routable_match.is_empty() { - break; - } - - layers.push(quote! { - #(#routable_match)* - }); + // Collect all routes matches + for route in &self.routes { + matches.push(route.routable_match(&self.layouts, &self.nests)); } - let index_iter = 0..layers.len(); - quote! { impl dioxus_router::routable::Routable for #name where Self: Clone { const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[ @@ -476,15 +486,8 @@ impl RouteEnum { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { let myself = self.clone(); - match level { - #( - #index_iter => { - match myself { - #layers - _ => None - } - }, - )* + match (level, myself) { + #(#matches)* _ => None } } @@ -521,14 +524,25 @@ impl SiteMapSegment { impl ToTokens for SiteMapSegment { fn to_tokens(&self, tokens: &mut TokenStream2) { let segment_type = &self.segment_type; - let children = &self.children; + let children = if let SegmentType::Child(ty) = &self.segment_type { + quote! { #ty::SITE_MAP } + } else { + let children = self + .children + .iter() + .map(|child| child.to_token_stream()) + .collect::>(); + quote! { + &[ + #(#children,)* + ] + } + }; tokens.extend(quote! { dioxus_router::routable::SiteMapSegment { segment_type: #segment_type, - children: &[ - #(#children,)* - ] + children: #children, } }); } @@ -538,6 +552,7 @@ enum SegmentType { Static(String), Dynamic(String), CatchAll(String), + Child(Type), } impl ToTokens for SegmentType { @@ -552,6 +567,9 @@ impl ToTokens for SegmentType { SegmentType::CatchAll(s) => { tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) }) } + SegmentType::Child(_) => { + tokens.extend(quote! { dioxus_router::routable::SegmentType::Child }) + } } } } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index dac505a8e..edfb7eb5a 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -3,6 +3,7 @@ use syn::parse::Parse; use syn::parse::ParseStream; use syn::parse_quote; use syn::Path; +use syn::Type; use syn::{Ident, LitStr}; use proc_macro2::TokenStream as TokenStream2; @@ -40,18 +41,28 @@ impl Parse for RouteArgs { } } +struct ChildArgs { + route: LitStr, +} + +impl Parse for ChildArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let route = input.parse::()?; + + Ok(ChildArgs { route }) + } +} + #[derive(Debug)] -pub struct Route { +pub(crate) struct Route { pub route_name: Ident, - pub comp_name: Path, - pub props_name: Path, + pub ty: RouteType, pub route: String, pub segments: Vec, pub query: Option, pub nests: Vec, pub layouts: Vec, - pub variant: syn::Variant, - fields: syn::FieldsNamed, + fields: Vec<(Ident, Type)>, } impl Route { @@ -63,62 +74,93 @@ impl Route { let route_attr = variant .attrs .iter() - .find(|attr| attr.path.is_ident("route")) - .ok_or_else(|| { - syn::Error::new_spanned( - variant.clone(), - "Routable variants must have a #[route(...)] attribute", - ) - })?; - + .find(|attr| attr.path.is_ident("route")); + let route; + let ty; let route_name = variant.ident.clone(); - let args = route_attr.parse_args::()?; - let route = args.route.value(); - let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name)); - let props_name = args.props_name.unwrap_or_else(|| { - let last = format_ident!( - "{}Props", - comp_name.segments.last().unwrap().ident.to_string() - ); - let mut segments = comp_name.segments.clone(); - segments.pop(); - segments.push(last.into()); - Path { - leading_colon: None, - segments, + match route_attr { + Some(attr) => { + let args = attr.parse_args::()?; + let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name)); + let props_name = args.props_name.unwrap_or_else(|| { + let last = format_ident!( + "{}Props", + comp_name.segments.last().unwrap().ident.to_string() + ); + let mut segments = comp_name.segments.clone(); + segments.pop(); + segments.push(last.into()); + Path { + leading_colon: None, + segments, + } + }); + ty = RouteType::Leaf { + component: comp_name, + props: props_name, + }; + route = args.route.value(); } - }); - - let named_fields = match &variant.fields { - syn::Fields::Named(fields) => fields, - _ => { - return Err(syn::Error::new_spanned( - variant.clone(), - "Routable variants must have named fields", - )) + None => { + if let Some(route_attr) = variant + .attrs + .iter() + .find(|attr| attr.path.is_ident("child")) + { + let args = route_attr.parse_args::()?; + route = args.route.value(); + match &variant.fields { + syn::Fields::Unnamed(fields) => { + if fields.unnamed.len() != 1 { + return Err(syn::Error::new_spanned( + variant.clone(), + "Routable variants with a #[child(...)] attribute must have exactly one field", + )); + } + ty = RouteType::Child(fields.unnamed[0].ty.clone()); + } + _ => { + return Err(syn::Error::new_spanned( + variant.clone(), + "Routable variants with a #[child(...)] attribute must have exactly one field", + )) + } + } + } else { + return Err(syn::Error::new_spanned( + variant.clone(), + "Routable variants must either have a #[route(...)] attribute or a #[child(...)] attribute", + )); + } } }; - let (route_segments, query) = parse_route_segments( - variant.ident.span(), - named_fields + let fields = match &variant.fields { + syn::Fields::Named(fields) => fields .named .iter() - .map(|f| (f.ident.as_ref().unwrap(), &f.ty)), - &route, - )?; + .map(|f| (f.ident.clone().unwrap(), f.ty.clone())) + .collect(), + _ => Vec::new(), + }; + + let (route_segments, query) = { + parse_route_segments( + variant.ident.span(), + fields.iter().map(|f| (&f.0, &f.1)), + &route, + )? + }; Ok(Self { - comp_name, - props_name, + ty, route_name, segments: route_segments, route, query, nests, layouts, - fields: named_fields.clone(), - variant, + fields, }) } @@ -129,101 +171,138 @@ impl Route { let write_segments = self.segments.iter().map(|s| s.write_segment()); let write_query = self.query.as_ref().map(|q| q.write()); - quote! { - Self::#name { #(#dynamic_segments,)* } => { - #(#write_layouts)* - #(#write_segments)* - #write_query + match &self.ty { + RouteType::Child(_) => { + quote! { + Self::#name(child) => { + use std::fmt::Display; + #(#write_layouts)* + #(#write_segments)* + child.fmt(f); + } + } + } + RouteType::Leaf { .. } => { + quote! { + Self::#name { #(#dynamic_segments,)* } => { + #(#write_layouts)* + #(#write_segments)* + #write_query + } + } } } } - pub fn routable_match( - &self, - layouts: &[Layout], - nests: &[Nest], - index: usize, - ) -> Option { + pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 { let name = &self.route_name; let name_str = name.to_string(); - let dynamic_segments = self.dynamic_segments(); - match index.cmp(&self.layouts.len()) { - std::cmp::Ordering::Less => { - let layout = self.layouts[index]; - let render_layout = layouts[layout.0].routable_match(nests); - // This is a layout - Some(quote! { + let mut tokens = TokenStream2::new(); + + // First match all layouts + for (idx, layout_id) in self.layouts.iter().copied().enumerate() { + let render_layout = layouts[layout_id.0].routable_match(nests); + let dynamic_segments = self.dynamic_segments(); + // This is a layout + tokens.extend(quote! { + #[allow(unused)] + (#idx, Self::#name { #(#dynamic_segments,)* }) => { + #render_layout + } + }); + } + + // Then match the route + let last_index = self.layouts.len(); + tokens.extend(match &self.ty { + RouteType::Child(_) => { + quote! { #[allow(unused)] - Self::#name { #(#dynamic_segments,)* } => { - #render_layout + (#last_index.., Self::#name(child_route)) => { + child_route.render(cx, level - #last_index) } - }) + } } - std::cmp::Ordering::Equal => { + RouteType::Leaf { component, props } => { + let dynamic_segments = self.dynamic_segments(); let dynamic_segments_from_route = self.dynamic_segments(); - let props_name = &self.props_name; - let comp_name = &self.comp_name; - // This is the final route - Some(quote! { + quote! { #[allow(unused)] - Self::#name { #(#dynamic_segments,)* } => { - let comp = #props_name { #(#dynamic_segments_from_route,)* }; - let dynamic = cx.component(#comp_name, comp, #name_str); + (#last_index, Self::#name { #(#dynamic_segments,)* }) => { + let comp = #props { #(#dynamic_segments_from_route,)* }; + let dynamic = cx.component(#component, comp, #name_str); render! { dynamic } } - }) + } } - _ => None, - } + }); + + tokens } fn dynamic_segments(&self) -> impl Iterator + '_ { - self.fields.named.iter().map(|f| { - let name = f.ident.as_ref().unwrap(); + self.fields.iter().map(|(name, _)| { quote! {#name} }) } pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 { - let segments = self.fields.named.iter().map(|f| { - let name = f.ident.as_ref().unwrap(); + match &self.ty { + RouteType::Child(ty) => { + let name = &self.route_name; - let mut from_route = false; - - for id in &self.nests { - let nest = &nests[id.0]; - if nest.dynamic_segments_names().any(|i| &i == name) { - from_route = true - } - } - for segment in &self.segments { - if let RouteSegment::Dynamic(other, ..) = segment { - if other == name { - from_route = true + quote! { + { + let mut trailing = String::new(); + for seg in segments { + trailing += seg; + trailing += "/"; + } + trailing.pop(); + #enum_name::#name(#ty::from_str(&trailing).unwrap()) } } } - if let Some(query) = &self.query { - if &query.ident == name { - from_route = true + RouteType::Leaf { .. } => { + let segments = self.fields.iter().map(|(name, _)| { + let mut from_route = false; + + for id in &self.nests { + let nest = &nests[id.0]; + if nest.dynamic_segments_names().any(|i| &i == name) { + from_route = true + } + } + for segment in &self.segments { + if let RouteSegment::Dynamic(other, ..) = segment { + if other == name { + from_route = true + } + } + } + if let Some(query) = &self.query { + if &query.ident == name { + from_route = true + } + } + + if from_route { + quote! {#name} + } else { + quote! {#name: Default::default()} + } + }); + let name = &self.route_name; + + quote! { + #enum_name::#name { + #(#segments,)* + } } } - - if from_route { - quote! {#name} - } else { - quote! {#name: Default::default()} - } - }); - let name = &self.route_name; - - quote! { - #enum_name::#name { - #(#segments,)* - } } } @@ -244,3 +323,9 @@ impl Route { } } } + +#[derive(Debug)] +pub(crate) enum RouteType { + Child(Type), + Leaf { component: Path, props: Path }, +} diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 5790c8386..523065e20 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -6,7 +6,7 @@ use syn::Ident; use crate::{ nest::{Nest, NestId}, redirect::Redirect, - route::Route, + route::{Route, RouteType}, segment::{static_segment_idx, RouteSegment}, }; @@ -334,11 +334,14 @@ impl<'a> RouteTreeSegmentData<'a> { let construct_variant = route.construct(nests, enum_name); let parse_query = route.parse_query(); - let insure_not_trailing = route - .segments - .last() - .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _))) - .unwrap_or(true); + let insure_not_trailing = match route.ty { + RouteType::Leaf { .. } => route + .segments + .last() + .map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _))) + .unwrap_or(true), + RouteType::Child(_) => false, + }; print_route_segment( route_segments.peekable(), diff --git a/packages/router/examples/static_generation.rs b/packages/router/examples/static_generation.rs index 30d503f7f..89359b4ab 100644 --- a/packages/router/examples/static_generation.rs +++ b/packages/router/examples/static_generation.rs @@ -28,22 +28,25 @@ async fn main() { .invalidate_after(Duration::from_secs(10)) .build(); + println!( + "SITE MAP:\n{}", + Route::SITE_MAP + .iter() + .flat_map(|route| route.flatten().into_iter()) + .map(|route| { + route + .iter() + .map(|segment| segment.to_string()) + .collect::>() + .join("") + }) + .collect::>() + .join("\n") + ); + pre_cache_static_routes::(&mut renderer) .await .unwrap(); - - for _ in 0..1_000_000 { - for id in 0..10 { - render_route( - &mut renderer, - Route::Post { id }, - &mut tokio::io::sink(), - |_| {}, - ) - .await - .unwrap(); - } - } } #[inline_props] @@ -82,6 +85,64 @@ fn Home(cx: Scope) -> Element { } } +mod inner { + use dioxus::prelude::*; + use dioxus_router::prelude::*; + + #[rustfmt::skip] + #[derive(Clone, Debug, PartialEq, Routable)] + pub enum Route { + #[nest("/blog")] + #[route("/")] + Blog {}, + #[route("/post/index")] + PostHome {}, + #[route("/post/:id")] + Post { + id: usize, + }, + #[end_nest] + #[route("/")] + Home {}, + } + + #[inline_props] + fn Blog(cx: Scope) -> Element { + render! { + div { + "Blog" + } + } + } + + #[inline_props] + fn Post(cx: Scope, id: usize) -> Element { + render! { + div { + "PostId: {id}" + } + } + } + + #[inline_props] + fn PostHome(cx: Scope) -> Element { + render! { + div { + "Post" + } + } + } + + #[inline_props] + fn Home(cx: Scope) -> Element { + render! { + div { + "Home" + } + } + } +} + #[rustfmt::skip] #[derive(Clone, Debug, PartialEq, Routable)] enum Route { @@ -97,4 +158,6 @@ enum Route { #[end_nest] #[route("/")] Home {}, + #[child("/hello_world")] + HelloWorldRoute(inner::Route) } diff --git a/packages/router/src/incremental.rs b/packages/router/src/incremental.rs index e4f867bbd..bc7713579 100644 --- a/packages/router/src/incremental.rs +++ b/packages/router/src/incremental.rs @@ -25,6 +25,7 @@ where let mut full_path = String::new(); for segment in &route { match segment { + SegmentType::Child => {} SegmentType::Static(s) => { full_path += "/"; full_path += s; @@ -43,6 +44,7 @@ where render_route(renderer, route, &mut tokio::io::sink(), |_| {}).await?; } Err(e) => { + log::info!("@ route: {}", full_path); log::error!("Error pre-caching static route: {}", e); } } diff --git a/packages/router/src/routable.rs b/packages/router/src/routable.rs index 4ae4563c3..269e89453 100644 --- a/packages/router/src/routable.rs +++ b/packages/router/src/routable.rs @@ -172,12 +172,15 @@ pub enum SegmentType { Dynamic(&'static str), /// A catch all route segment CatchAll(&'static str), + /// A child router + Child, } impl Display for SegmentType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { SegmentType::Static(s) => write!(f, "/{}", s), + SegmentType::Child => Ok(()), SegmentType::Dynamic(s) => write!(f, "/:{}", s), SegmentType::CatchAll(s) => write!(f, "/:...{}", s), }