This commit is contained in:
Greg Johnston 2024-04-25 13:12:29 -04:00
parent 782cb93743
commit 789eef914d
15 changed files with 353 additions and 442 deletions

View file

@ -13,7 +13,7 @@ lazy_static = "1"
leptos = { path = "../../leptos"}
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
routing = { path = "../../routing" }
log = "0.4"
serde = { version = "1", features = ["derive"] }
thiserror = "1"
@ -24,16 +24,14 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"], optio
wasm-bindgen = "0.2"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]

View file

@ -1,7 +1,16 @@
use lazy_static::lazy_static;
use leptos::*;
use leptos::prelude::*;
use leptos::{
component, server, server::Resource, server_fn::ServerFnError, suspend,
view, ErrorBoundary, IntoView, Params, Suspense,
};
use leptos_meta::*;
use leptos_router::*;
use routing::{
components::{Route, Router, Routes},
hooks::use_params,
params::Params,
ParamSegment, SsrMode, StaticSegment,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -15,21 +24,22 @@ pub fn App() -> impl IntoView {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Router fallback>
<Router>
<main>
<Routes>
// TODO should fallback be on Routes or Router?
<Routes fallback>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=HomePage/>
<Route path=StaticSegment("") view=HomePage/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
path=(StaticSegment("post"), ParamSegment("id"))
view=Post
ssr=SsrMode::Async
/>
<Route
path="/post_in_order/:id"
path=(StaticSegment("post_in_order"), ParamSegment("id"))
view=Post
ssr=SsrMode::InOrder
/>
@ -42,20 +52,20 @@ pub fn App() -> impl IntoView {
#[component]
fn HomePage() -> impl IntoView {
// load the posts
let posts =
create_resource(|| (), |_| async { list_post_metadata().await });
let posts_view = move || {
posts.and_then(|posts| {
posts.iter()
let posts = Resource::new_serde(|| (), |_| list_post_metadata());
let posts_view = suspend!(
posts.await.map(|posts| {
posts.into_iter()
.map(|post| view! {
<li>
<a href=format!("/post/{}", post.id)>{&post.title}</a> "|"
<a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a>
<a href=format!("/post/{}", post.id)>{post.title.clone()}</a>
"|"
<a href=format!("/post_in_order/{}", post.id)>{post.title} "(in order)"</a>
</li>
})
.collect_view()
.collect::<Vec<_>>()
})
};
);
view! {
<h1>"My Great Blog"</h1>
@ -80,7 +90,7 @@ fn Post() -> impl IntoView {
.map_err(|_| PostError::InvalidId)
})
};
let post_resource = create_resource(id, |id| async move {
let post_resource = Resource::new_serde(id, |id| async move {
match id {
Err(e) => Err(e),
Ok(id) => get_post(id)
@ -90,45 +100,42 @@ fn Post() -> impl IntoView {
}
});
let post = move || match post_resource.get() {
Some(Ok(Ok(v))) => Ok(v),
_ => Err(PostError::ServerError),
};
let post_view = move || {
post().map(|post| {
view! {
// render content
<h1>{&post.title}</h1>
<p>{&post.content}</p>
let post_view = suspend!({
match post_resource.await {
Ok(Ok(post)) => Ok(view! {
<h1>{post.title.clone()}</h1>
<p>{post.content.clone()}</p>
// since we're using async rendering for this page,
// this metadata should be included in the actual HTML <head>
// when it's first served
<Title text=post.title.clone()/>
<Meta name="description" content=post.content.clone()/>
}
})
};
<Title text=post.title/>
<Meta name="description" content=post.content/>
}),
_ => Err(PostError::ServerError),
}
});
view! {
<Suspense fallback=move || view! { <p>"Loading post..."</p> }>
<ErrorBoundary fallback=|errors| {
let errors = errors.clone();
view! {
<div class="error">
<h1>"Something went wrong."</h1>
<ul>
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { <li>{error.to_string()} </li> })
.collect_view()
}
{move || {
errors
.get()
.into_iter()
.map(|(_, error)| view! { <li>{error.to_string()}</li> })
.collect::<Vec<_>>()
}}
</ul>
</div>
}
}>
{post_view}
</ErrorBoundary>
}>{post_view}</ErrorBoundary>
</Suspense>
}
}

View file

@ -5,7 +5,7 @@ use axum::{
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use leptos::{config::LeptosOptions, view};
use tower::ServiceExt;
use tower_http::services::ServeDir;

View file

@ -12,5 +12,5 @@ pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
leptos::hydrate_body(App);
}

View file

@ -2,7 +2,7 @@
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::{logging::log, *};
use leptos::{logging::log, config::get_configuration, view, HydrationScripts};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@ -19,7 +19,28 @@ async fn main() {
// _ = ListPostMetadata::register();
let app = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || {
use leptos::prelude::*;
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
// <AutoReload options=app_state.leptos_options.clone() />
<HydrationScripts options=leptos_options.clone()/>
<link rel="stylesheet" id="leptos" href="/pkg/benwis_leptos.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
<body>
<App/>
</body>
</html>
}
}})
.fallback(file_and_error_handler)
.with_state(leptos_options);

View file

@ -39,7 +39,6 @@ ssr = [
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"dep:leptos_axum",
]

View file

@ -53,6 +53,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
reactive_graph::{computed::ScopedFuture, owner::Owner},
tachys::ssr::StreamBuilder,
IntoView,
};
use leptos_meta::{MetaContext, ServerMetaContext};
@ -458,7 +459,7 @@ pub fn render_app_to_stream<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
render_app_to_stream_with_context(options, || {}, app_fn)
}
@ -480,7 +481,7 @@ pub fn render_route<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
render_route_with_context(options, paths, || {}, app_fn)
}
@ -549,7 +550,7 @@ pub fn render_app_to_stream_in_order<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
render_app_to_stream_in_order_with_context(options, || {}, app_fn)
}
@ -591,7 +592,7 @@ pub fn render_app_to_stream_with_context<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
render_app_to_stream_with_context_and_replace_blocks(
options,
@ -619,7 +620,7 @@ pub fn render_route_with_context<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
let ooo = render_app_to_stream_with_context(
LeptosOptions::from_ref(&options),
@ -652,11 +653,8 @@ where
.as_str();
// 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
// search for this every time
let listing: &AxumRouteListing = paths
.iter()
// TODO this should be cached rather than recalculating the Axum version of the path
.find(|r| r.path() == path)
.unwrap_or_else(|| {
let listing: &AxumRouteListing =
paths.iter().find(|r| r.path() == path).unwrap_or_else(|| {
panic!(
"Failed to find the route {path} requested by the user. \
This suggests that the routing rules in the Router that \
@ -706,144 +704,11 @@ pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
move |req: Request<Body>| {
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
Box::pin(Sandboxed::new(async move {
let meta_context = ServerMetaContext::new();
let stream = owner.with(|| {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
let path = req.uri().path_and_query().unwrap().as_str();
let full_path = format!("http://leptos.dev{path}");
let (_, req_parts) = generate_request_and_parts(req);
provide_contexts(
&full_path,
&meta_context,
req_parts,
default_res_options,
);
add_context();
// run app
let app = app_fn();
// convert app to appropriate response type
let app_stream = app.to_html_stream_out_of_order();
// TODO nonce
let shared_context = Owner::current_shared_context().unwrap();
let chunks = Box::pin(
shared_context
.pending_data()
.unwrap()
.map(|chunk| format!("<script>{chunk}</script>")),
);
futures::stream::select(app_stream, chunks)
});
let stream = meta_context.inject_meta_context(stream).await;
Html(Body::from_stream(Sandboxed::new(
stream
.map(|chunk| Ok(chunk) as Result<String, std::io::Error>)
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
drop(owner);
Ok(Default::default())
})),
)))
.into_response()
}))
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn generate_response(
res_options: ResponseOptions,
rx: Receiver<String>,
) -> Response<Body> {
todo!() /*
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
// Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
// Extract the resources now that they've been rendered
let res_options = res_options.0.read();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream);
let mut res =
Body::from_stream(Box::pin(complete_stream) as PinnedHtmlStream)
.into_response();
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let headers = res.headers_mut();
let mut res_headers = res_options.headers.clone();
headers.extend(res_headers.drain());
if !headers.contains_key(header::CONTENT_TYPE) {
// Set the Content Type headers on all responses. This makes Firefox show the page source
// without complaining
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
);
}
res*/
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn forward_stream(
options: &LeptosOptions,
res_options2: ResponseOptions,
bundle: impl Stream<Item = String> + 'static,
mut tx: Sender<String>,
) {
/*let mut shell = Box::pin(bundle);
let first_app_chunk = shell.next().await.unwrap_or_default();
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>().as_ref());
_ = tx.send(head).await;
_ = tx.send(first_app_chunk).await;
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
_ = tx.send(tail.to_string()).await;
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>().unwrap();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
tx.close_channel();*/
handle_response(options, additional_context, app_fn, |app| {
Box::pin(app.to_html_stream_out_of_order())
})
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
@ -885,55 +750,103 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
|req| todo!()
/*
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
handle_response(options, additional_context, app_fn, |app| {
Box::pin(app.to_html_stream_in_order())
})
}
async move {
fn handle_response<IV>(
options: LeptosOptions,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(IV) -> Pin<Box<dyn Stream<Item = String> + Send>>,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
{
move |req: Request<Body>| {
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let res_options = ResponseOptions::default();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
Box::pin(Sandboxed::new(async move {
let meta_context = ServerMetaContext::new();
let stream = owner.with(|| {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
let full_path = format!("http://leptos.dev{path}");
let (_, req_parts) = generate_request_and_parts(req);
provide_contexts(
&full_path,
&meta_context,
req_parts,
res_options.clone(),
);
add_context();
let (tx, rx) = futures::channel::mpsc::channel(8);
let current_span = tracing::Span::current();
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (parts, _) = req.into_parts();
move || {
provide_contexts(full_path, parts, default_res_options);
app_fn().into_view()
}
};
// run app
let app = app_fn();
let (bundle, runtime) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| generate_head_metadata_separated().1.into(),
add_context,
);
// convert app to appropriate response type
let app_stream = stream_builder(app);
forward_stream(&options, res_options2, bundle, tx).await;
// TODO nonce
runtime.dispose();
}.instrument(current_span));
let shared_context = Owner::current_shared_context().unwrap();
let chunks = Box::pin(
shared_context
.pending_data()
.unwrap()
.map(|chunk| format!("<script>{chunk}</script>")),
);
futures::stream::select(app_stream, chunks)
});
generate_response(res_options3, rx).await
let stream = meta_context.inject_meta_context(stream).await;
// TODO test this
/*if let Some(status) = res_options.status {
*res.status_mut() = status
}
})
}*/
let headers = res.headers_mut();
let mut res_headers = res_options.headers.clone();
headers.extend(res_headers.drain());
if !headers.contains_key(header::CONTENT_TYPE) {
// Set the Content Type headers on all responses. This makes Firefox show the page source
// without complaining
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
);
}*/
Html(Body::from_stream(Sandboxed::new(
stream
.map(|chunk| Ok(chunk) as Result<String, std::io::Error>)
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
drop(owner);
Ok(Default::default())
})),
)))
.into_response()
}))
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
@ -1012,12 +925,12 @@ pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<String>> + Send + 'static>>
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
render_app_async_with_context(options, || {}, app_fn)
}
@ -1060,96 +973,15 @@ pub fn render_app_async_stream_with_context<IV>(
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
move |req: Request<Body>| {
todo!()
/*Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
handle_response(options, additional_context, app_fn, |app| {
Box::pin(futures::stream::once(async move {
use futures::StreamExt;
async move {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(
full_path,
req_parts,
default_res_options,
);
app_fn().into_view()
}
};
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| "".into(),
add_context,
);
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>().unwrap();
let html =
build_async_response(stream, &options, runtime).await;
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
});
let html = rx.await.expect("to complete HTML rendering");
let res_options = res_options3.0.read();
let complete_stream =
futures::stream::iter([Ok(Bytes::from(html))]);
let mut res = Body::from_stream(
Box::pin(complete_stream) as PinnedHtmlStream
)
.into_response();
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let headers = res.headers_mut();
let mut res_headers = res_options.headers.clone();
headers.extend(res_headers.drain());
// This one doesn't use generate_response(), so we need to do this separately
if !headers.contains_key(header::CONTENT_TYPE) {
// Set the Content Type headers on all responses. This makes Firefox show the page source
// without complaining
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("text/html; charset=utf-8")
.unwrap(),
);
}
res
}
})*/
}
app.to_html_stream_out_of_order().collect::<String>().await
}))
})
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
@ -1185,84 +1017,20 @@ pub fn render_app_async_with_context<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<String>> + Send + 'static>>
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
IV: IntoView + 'static,
{
|_| todo!()
/* move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
handle_response(options, additional_context, app_fn, |app| {
Box::pin(futures::stream::once(async move {
use futures::StreamExt;
async move {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(
full_path,
req_parts,
default_res_options,
);
app_fn().into_view()
}
};
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| "".into(),
add_context,
);
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>().unwrap();
let html =
build_async_response(stream, &options, runtime).await;
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
});
let html = rx.await.expect("to complete HTML rendering");
let mut res = Response::new(html);
let res_options = res_options3.0.read();
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let mut res_headers = res_options.headers.clone();
res.headers_mut().extend(res_headers.drain());
res
}
})
}*/
app.to_html_stream_out_of_order().collect::<String>().await
}))
})
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically

View file

@ -168,6 +168,7 @@ impl ServerMetaContext {
self,
mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send {
println!("injecting meta context!");
let mut first_chunk = stream.next().await.unwrap_or_default();
let meta_buf =
@ -185,6 +186,7 @@ impl ServerMetaContext {
let mut buf = String::with_capacity(
first_chunk.len() + title_len + meta_buf.len(),
);
println!("first_chunk = {first_chunk:?}");
let head_loc = first_chunk
.find("</head>")
.expect("you are using leptos_meta without a </head> tag");

View file

@ -1,11 +1,12 @@
use crate::{
hooks::use_navigate,
location::{
BrowserUrl, Location, LocationChange, LocationProvider, State, Url,
BrowserUrl, Location, LocationChange, LocationProvider, RequestUrl,
State, Url,
},
navigate::{NavigateOptions, UseNavigate},
resolve_path::resolve_path,
MatchNestedRoutes, NestedRoute, NestedRoutesView, Routes,
MatchNestedRoutes, NestedRoute, NestedRoutesView, Routes, SsrMode,
};
use leptos::{
children::{ToChildren, TypedChildren},
@ -69,10 +70,18 @@ pub fn Router<Chil>(
where
Chil: IntoView,
{
let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
let current_url = location.as_url().clone();
let current_url = if Owner::current_shared_context()
.map(|sc| sc.is_browser())
.unwrap_or(false)
{
let location = BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
location.as_url().clone()
} else {
let req = use_context::<RequestUrl>().expect("no RequestUrl provided");
let parsed = req.parse().expect("could not parse RequestUrl");
ArcRwSignal::new(parsed)
};
// provide router context
let state = ArcRwSignal::new(State::new(None));
@ -228,11 +237,12 @@ where
pub fn Route<Segments, View, ViewFn>(
path: Segments,
view: ViewFn,
#[prop(optional)] ssr: SsrMode,
) -> NestedRoute<Segments, (), (), ViewFn, Dom>
where
ViewFn: Fn() -> View,
{
NestedRoute::new(path, view)
NestedRoute::new(path, view, ssr)
}
#[component]
@ -240,12 +250,13 @@ pub fn ParentRoute<Segments, View, Children, ViewFn>(
path: Segments,
view: ViewFn,
children: RouteChildren<Children>,
#[prop(optional)] ssr: SsrMode,
) -> NestedRoute<Segments, Children, (), ViewFn, Dom>
where
ViewFn: Fn() -> View,
{
let children = children.into_inner();
NestedRoute::new(path, view).child(children)
NestedRoute::new(path, view, ssr).child(children)
}
/// Redirects the user to a new URL, whether on the client side or on the server

View file

@ -24,16 +24,13 @@ impl Default for RequestUrl {
}
impl RequestUrl {
pub fn parse(url: &str) -> Result<Url, url::ParseError> {
Self::parse_with_base(url, BASE)
pub fn parse(&self) -> Result<Url, url::ParseError> {
self.parse_with_base(BASE)
}
pub fn parse_with_base(
url: &str,
base: &str,
) -> Result<Url, url::ParseError> {
pub fn parse_with_base(&self, base: &str) -> Result<Url, url::ParseError> {
let base = url::Url::parse(base)?;
let url = url::Url::options().base_url(Some(&base)).parse(url)?;
let url = url::Url::options().base_url(Some(&base)).parse(&self.0)?;
let search_params = url
.query_pairs()

View file

@ -6,6 +6,7 @@ pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use crate::{Method, SsrMode};
pub use horizontal::*;
pub use nested::*;
use std::{borrow::Cow, marker::PhantomData};
@ -91,7 +92,7 @@ where
&self,
) -> (
Option<&str>,
impl IntoIterator<Item = Vec<PathSegment>> + '_,
impl IntoIterator<Item = GeneratedRouteData> + '_,
) {
(self.base.as_deref(), self.children.generate_routes())
}
@ -137,7 +138,13 @@ where
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_;
) -> impl IntoIterator<Item = GeneratedRouteData> + '_;
}
#[derive(Default)]
pub(crate) struct GeneratedRouteData {
pub segments: Vec<PathSegment>,
pub ssr_mode: SsrMode,
}
#[cfg(test)]

View file

@ -2,7 +2,7 @@ use super::{
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
PossibleRouteMatch, RouteMatchId,
};
use crate::{ChooseView, MatchParams};
use crate::{ChooseView, MatchParams, SsrMode, GeneratedRouteData};
use core::{fmt, iter};
use std::{borrow::Cow, marker::PhantomData, sync::atomic::{AtomicU16, Ordering}};
use either_of::Either;
@ -23,18 +23,19 @@ pub struct NestedRoute<Segments, Children, Data, ViewFn, R> {
pub data: Data,
pub view: ViewFn,
pub rndr: PhantomData<R>,
pub ssr_mode: SsrMode
}
impl<Segments, Children, Data, ViewFn, R> Clone for NestedRoute<Segments, Children, Data, ViewFn, R> where Segments: Clone, Children: Clone, Data: Clone, ViewFn: Clone{
fn clone(&self) -> Self {
Self {
id: self.id,segments: self.segments.clone(),children: self.children.clone(),data: self.data.clone(), view: self.view.clone(), rndr: PhantomData
id: self.id,segments: self.segments.clone(),children: self.children.clone(),data: self.data.clone(), view: self.view.clone(), rndr: PhantomData, ssr_mode: self.ssr_mode
}
}
}
impl<Segments, ViewFn, R> NestedRoute<Segments, (), (), ViewFn, R> {
pub fn new<View>(path: Segments, view: ViewFn) -> Self
pub fn new<View>(path: Segments, view: ViewFn, ssr_mode: SsrMode) -> Self
where
ViewFn: Fn() -> View,
R: Renderer + 'static,
@ -46,6 +47,7 @@ impl<Segments, ViewFn, R> NestedRoute<Segments, (), (), ViewFn, R> {
data: (),
view,
rndr: PhantomData,
ssr_mode
}
}
}
@ -61,6 +63,7 @@ impl<Segments, Data, ViewFn, R> NestedRoute<Segments, (), Data, ViewFn, R> {
data,
view,
rndr,
ssr_mode,
..
} = self;
NestedRoute {
@ -69,6 +72,7 @@ impl<Segments, Data, ViewFn, R> NestedRoute<Segments, (), Data, ViewFn, R> {
children: Some(child),
data,
view,
ssr_mode,
rndr,
}
}
@ -219,14 +223,31 @@ where
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let children = self.children.as_ref();
let ssr_mode = self.ssr_mode;
match children {
None => Either::Left(iter::once(segment_routes)),
None => Either::Left(iter::once(GeneratedRouteData {
segments: segment_routes,
ssr_mode
})),
Some(children) => {
Either::Right(children.generate_routes().into_iter())
Either::Right(children.generate_routes().into_iter().map(move |child| {
if child.ssr_mode > ssr_mode {
GeneratedRouteData {
segments: child.segments ,
ssr_mode: child.ssr_mode,
}
} else {
GeneratedRouteData {
segments: child.segments ,
ssr_mode,
}
}
}))
}
}
}

View file

@ -1,5 +1,5 @@
use super::{MatchInterface, MatchNestedRoutes, PathSegment, RouteMatchId};
use crate::{ChooseView, MatchParams};
use crate::{ChooseView, GeneratedRouteData, MatchParams};
use core::iter;
use either_of::*;
use std::{any::Any, borrow::Cow};
@ -55,8 +55,11 @@ where
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
iter::once(vec![PathSegment::Unit])
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
iter::once(GeneratedRouteData {
segments: vec![PathSegment::Unit],
..Default::default()
})
}
}
@ -115,7 +118,7 @@ where
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
self.0.generate_routes()
}
}
@ -207,7 +210,7 @@ where
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
#![allow(non_snake_case)]
let (A, B) = &self;
@ -304,7 +307,7 @@ macro_rules! tuples {
fn generate_routes(
&self,
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
#![allow(non_snake_case)]
let ($($ty,)*) = &self;

View file

@ -1,9 +1,10 @@
use crate::{
location::{Location, Url},
location::{Location, RequestUrl, Url},
matching::Routes,
params::ParamsMap,
resolve_path::resolve_path,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId,
};
use either_of::Either;
use leptos::{component, oco::Oco, IntoView};
@ -16,6 +17,7 @@ use reactive_graph::{
};
use std::{
borrow::Cow,
iter,
marker::PhantomData,
mem,
sync::{
@ -25,10 +27,11 @@ use std::{
};
use tachys::{
renderer::Renderer,
ssr::StreamBuilder,
view::{
any_view::{AnyView, AnyViewState, IntoAny},
either::EitherState,
Mountable, Render, RenderHtml,
Mountable, Position, Render, RenderHtml,
},
};
@ -150,12 +153,86 @@ where
self
}
fn to_html_with_buf(
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
// if this is being run on the server for the first time, generating all possible routes
if RouteList::is_generating() {
// add routes
let (base, routes) = self.routes.generate_routes();
let mut routes = routes
.into_iter()
.map(|data| {
let path = base
.into_iter()
.flat_map(|base| {
iter::once(PathSegment::Static(
base.to_string().into(),
))
})
.chain(data.segments)
.collect::<Vec<_>>();
RouteListing::new(
path,
data.ssr_mode,
// TODO methods
[Method::Get],
// TODO static data
None,
)
})
.collect::<Vec<_>>();
println!("routes = {routes:#?}");
// add fallback
// TODO fix: causes overlapping route issues on Axum
/*routes.push(RouteListing::new(
[PathSegment::Static(
base.unwrap_or_default().to_string().into(),
)],
SsrMode::Async,
[
Method::Get,
Method::Post,
Method::Put,
Method::Patch,
Method::Delete,
],
None,
));*/
RouteList::register(RouteList::from(routes));
} else {
let outer_owner = Owner::current()
.expect("creating Router, but no Owner was found");
let url = use_context::<RequestUrl>()
.expect("could not find request URL in context");
// TODO base
let url = if let Some(base) = &self.base {
url.parse_with_base(base.as_ref())
} else {
url.parse()
}
.expect("could not parse URL");
// TODO query params
let new_match = self.routes.match_route(url.path());
/*match new_match {
Some(matched) => {
Either::Left(NestedRouteView::new(&outer_owner, matched))
}
_ => Either::Right((self.fallback)()),
}
.to_html_with_buf(buf, position)*/
todo!()
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut String,
position: &mut tachys::view::Position,
) {
todo!()
buf: &mut StreamBuilder,
position: &mut Position,
) where
Self: Sized,
{
}
fn hydrate<const FROM_SERVER: bool>(

View file

@ -212,7 +212,7 @@ where
let (base, routes) = self.routes.generate_routes();
let mut routes = routes
.into_iter()
.map(|segments| {
.map(|data| {
let path = base
.into_iter()
.flat_map(|base| {
@ -220,13 +220,13 @@ where
base.to_string().into(),
))
})
.chain(segments)
.chain(data.segments)
.collect::<Vec<_>>();
// TODO add non-defaults for mode, etc.
RouteListing::new(
path,
SsrMode::OutOfOrder,
[Method::Get],
data.ssr_mode,
data.methods,
None,
)
})