Files
dioxus/packages/router-macro/src/route.rs
T
Jonathan Kelley bdeedc13eb feat: router-based wasm bundle splitting (#3683)
* feat: wasm-split base implementation

* kinda sorta works with the cli

* okay great it actually works

* fix: cache busting for chunks

* cleaned up version

* it works with the router

* wip: migrate to a faster linear solution using id remapping

* add a good harness

* it works completely on the docsite

* the super tight build works too

* make parallelizable

* works on the docsite but not the harness

* light cleanups, fix for both harness and docsite

* make codegen smaller by extract some shared functions to prevent monos

* chunking works, but tbd on tuning heuristics

* extract used module

* wip: cleaning up module

* wip: much better understanding of how imports work

* fix the call graph

* working again...

* wip: thinking about ifuncs

* ifunc approach works

* yes the ifunc approach works!!

* it all works completely even with chunks

* small cleanups

* comments

* actually locate all of the symbols

* clean up final implementation

* clean up tomls, revert some mono changs

* clean up, add some more comments / docs in places

* hoist more packages

* clean up impl a bit more

* clean cli

* move harness to playwright

* update playwright

* extract codgen

* add lazy to components

* Add docs to split loader

* clippy

* typos

* clippy, don't generate splits if not on wasm

* whoops, const

* Typos and docs

* enable optimizations in ci

* add optimizations to ci

* add wasm-split router feature

* parallel playwright?

* fix no case check

* disable pre-compress... think that might be the issue

* better logging, no default pre-compress, caching of shared symbols
* Fix: compressing assets

* fix non-interactive logging for serve

* cache playwright even on failure, use the cli with optimizations

* fix prebuild

* fix playwright webserver
2025-02-13 18:54:38 -08:00

464 lines
17 KiB
Rust

use quote::{format_ident, quote};
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_quote;
use syn::Field;
use syn::Path;
use syn::Type;
use syn::{Ident, LitStr};
use proc_macro2::TokenStream as TokenStream2;
use crate::hash::HashFragment;
use crate::layout::Layout;
use crate::layout::LayoutId;
use crate::nest::Nest;
use crate::nest::NestId;
use crate::query::QuerySegment;
use crate::segment::create_error_type;
use crate::segment::parse_route_segments;
use crate::segment::RouteSegment;
struct RouteArgs {
route: LitStr,
comp_name: Option<Path>,
}
impl Parse for RouteArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let route = input.parse::<LitStr>()?;
Ok(RouteArgs {
route,
comp_name: {
let _ = input.parse::<syn::Token![,]>();
input.parse().ok()
},
})
}
}
struct ChildArgs {
route: LitStr,
}
impl Parse for ChildArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let route = input.parse::<LitStr>()?;
Ok(ChildArgs { route })
}
}
#[derive(Debug)]
pub(crate) struct Route {
pub route_name: Ident,
pub ty: RouteType,
pub route: String,
pub segments: Vec<RouteSegment>,
pub query: Option<QuerySegment>,
pub hash: Option<HashFragment>,
pub nests: Vec<NestId>,
pub layouts: Vec<LayoutId>,
fields: Vec<(Ident, Type)>,
}
impl Route {
pub(crate) fn parse(
nests: Vec<NestId>,
layouts: Vec<LayoutId>,
variant: syn::Variant,
) -> syn::Result<Self> {
let route_attr = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("route"));
let route;
let ty;
let route_name = variant.ident.clone();
match route_attr {
Some(attr) => {
let args = attr.parse_args::<RouteArgs>()?;
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
ty = RouteType::Leaf {
component: comp_name,
};
route = args.route.value();
}
None => {
if let Some(route_attr) = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("child"))
{
let args = route_attr.parse_args::<ChildArgs>()?;
route = args.route.value();
match &variant.fields {
syn::Fields::Named(fields) => {
// find either a field with #[child] or a field named "child"
let child_field = fields.named.iter().find(|f| {
f.attrs
.iter()
.any(|attr| attr.path().is_ident("child"))
|| *f.ident.as_ref().unwrap() == "child"
});
match child_field{
Some(child) => {
ty = RouteType::Child(child.clone());
}
None => {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants with a #[child(..)] attribute must have a field named \"child\" or a field with a #[child] attribute",
));
}
}
}
_ => {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants with a #[child(..)] attribute must have named fields",
))
}
}
} else {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants must either have a #[route(..)] attribute or a #[child(..)] attribute",
));
}
}
};
let fields = match &variant.fields {
syn::Fields::Named(fields) => fields
.named
.iter()
.filter_map(|f| {
if let RouteType::Child(child) = &ty {
if f.ident == child.ident {
return None;
}
}
Some((f.ident.clone().unwrap(), f.ty.clone()))
})
.collect(),
_ => Vec::new(),
};
let (route_segments, query, hash) = {
parse_route_segments(
variant.ident.span(),
fields.iter().map(|f| (&f.0, &f.1)),
&route,
)?
};
Ok(Self {
ty,
route_name,
segments: route_segments,
route,
query,
hash,
nests,
layouts,
fields,
})
}
pub(crate) fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
let name = &self.route_name;
let dynamic_segments = self.dynamic_segments();
let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
let write_hash = self.hash.as_ref().map(|q| q.write());
match &self.ty {
RouteType::Child(field) => {
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
let write_segments = self.segments.iter().map(|s| s.write_segment());
let child = field.ident.as_ref().unwrap();
quote! {
Self::#name { #(#dynamic_segments,)* #child } => {
use std::fmt::Display;
use std::fmt::Write;
let mut route = String::new();
{
let f = &mut route;
#(#write_nests)*
#(#write_segments)*
}
if route.ends_with('/') {
route.pop();
}
f.write_str(&route)?;
#child.fmt(f)?;
}
}
}
RouteType::Leaf { .. } => {
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
let write_segments = self.segments.iter().map(|s| s.write_segment());
quote! {
Self::#name { #(#dynamic_segments,)* } => {
#(#write_nests)*
#(#write_segments)*
#write_query
#write_hash
}
}
}
}
}
pub(crate) fn routable_match(
&self,
layouts: &[Layout],
nests: &[Nest],
router_name: &Ident,
) -> TokenStream2 {
let name = &self.route_name;
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();
let mut field_name = None;
if let RouteType::Child(field) = &self.ty {
field_name = field.ident.as_ref();
}
let field_name = field_name.map(|f| quote!(#f,));
// This is a layout
tokens.extend(quote! {
#[allow(unused)]
(#idx, Self::#name { #(#dynamic_segments,)* #field_name .. }) => {
#render_layout
}
});
}
// Then match the route
let last_index = self.layouts.len();
tokens.extend(match &self.ty {
RouteType::Child(field) => {
let field_name = field.ident.as_ref().unwrap();
quote! {
#[allow(unused)]
(#last_index.., Self::#name { #field_name, .. }) => {
rsx! {
dioxus_router::components::child_router::ChildRouter {
route: #field_name,
// Try to parse the current route as a parent route, and then match it as a child route
parse_route_from_root_route: |__route| if let Ok(__route) = __route.parse() {
if let Self::#name { #field_name, .. } = __route {
Some(#field_name)
} else {
None
}
} else {
None
},
// Try to parse the child route and turn it into a parent route
format_route_as_root_route: |#field_name| Self::#name { #field_name: #field_name }.to_string(),
}
}
}
}
}
RouteType::Leaf { component } => {
let dynamic_segments = self.dynamic_segments();
let dynamic_segments_from_route = self.dynamic_segments();
/*
The implementation of this is pretty gnarly/gross.
We achieve the bundle splitting by wrapping the incoming function in a new component
that suspends based on an internal lazy loader. This lets us use suspense features
without breaking the rules of hooks. The router derive is quite complex so this shoves
the complexity towards the "leaf" of the codegen rather to its core. In the future though,
we should think about restructuring the router macro completely since its codegen
makes up nearly 30-40% of the binary size in the dioxus docsite.
*/
use sha2::Digest;
let dynamic_segments_receiver = self.dynamic_segments();
let dynamic_segments_from_route_ = self.dynamic_segments();
let dynamic_segments_from_route__ = self.dynamic_segments();
let unique_identifier = base16::encode_lower(
&sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))[..16],
);
let module_name = format_ident!("module{}{unique_identifier}", name).to_string();
let comp_name = format_ident!("route{}{unique_identifier}", name);
quote! {
#[allow(unused)]
(#last_index, Self::#name { #(#dynamic_segments,)* }) => {
dioxus::config_macros::maybe_wasm_split! {
if wasm_split {
{
fn #comp_name(args: #router_name) -> Element {
match args {
#router_name::#name { #(#dynamic_segments_from_route_,)* } => {
rsx! {
#component {
#(#dynamic_segments_from_route__: #dynamic_segments_from_route__,)*
}
}
}
_ => unreachable!()
}
}
#[component]
fn LoaderInner(args: NoPartialEq<#router_name>) -> Element {
static MODULE: wasm_split::LazyLoader<#router_name, Element> =
wasm_split::lazy_loader!(extern #module_name fn #comp_name(props: #router_name) -> Element);
use_resource(|| async move { MODULE.load().await }).suspend()?;
MODULE.call(args.0).unwrap()
}
struct NoPartialEq<T>(T);
impl<T: Clone> Clone for NoPartialEq<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: std::fmt::Display> std::fmt::Display for NoPartialEq<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<T> PartialEq for NoPartialEq<T> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
rsx! {
LoaderInner {
args: NoPartialEq(#router_name::#name { #(#dynamic_segments_receiver,)* } )
}
}
}
} else {
{
rsx! {
#component {
#(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
}
}
}
}
}
}
}
}
});
tokens
}
fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.fields.iter().map(|(name, _)| {
quote! {#name}
})
}
pub(crate) fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
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 segment.name().as_ref() == Some(name) {
from_route = true
}
}
if let Some(query) = &self.query {
if query.contains_ident(name) {
from_route = true
}
}
if let Some(hash) = &self.hash {
if hash.contains_ident(name) {
from_route = true
}
}
if from_route {
quote! {#name}
} else {
quote! {#name: Default::default()}
}
});
match &self.ty {
RouteType::Child(field) => {
let name = &self.route_name;
let child_name = field.ident.as_ref().unwrap();
quote! {
#enum_name::#name {
#child_name,
#(#segments,)*
}
}
}
RouteType::Leaf { .. } => {
let name = &self.route_name;
quote! {
#enum_name::#name {
#(#segments,)*
}
}
}
}
}
pub(crate) fn error_ident(&self) -> Ident {
format_ident!("{}ParseError", self.route_name)
}
pub(crate) fn error_type(&self) -> TokenStream2 {
let error_name = self.error_ident();
let child_type = match &self.ty {
RouteType::Child(field) => Some(&field.ty),
RouteType::Leaf { .. } => None,
};
create_error_type(&self.route, error_name, &self.segments, child_type)
}
pub(crate) fn parse_query(&self) -> TokenStream2 {
match &self.query {
Some(query) => query.parse(),
None => quote! {},
}
}
pub(crate) fn parse_hash(&self) -> TokenStream2 {
match &self.hash {
Some(hash) => hash.parse(),
None => quote! {},
}
}
}
#[derive(Debug)]
pub(crate) enum RouteType {
Child(Field),
Leaf { component: Path },
}