Merge branch 'leptos_dom_v2' of https://github.com/jquesada2016/leptos into leptos_dom_v2

This commit is contained in:
Jose Quesada 2022-12-12 12:18:29 -06:00
commit 5881fb9064
18 changed files with 620 additions and 137 deletions

View File

@ -0,0 +1 @@
.leptos.kdl

View File

@ -16,18 +16,18 @@ serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../../leptos/integrations/actix", optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
gloo-net = { git = "https://github.com/rustwasm/gloo" }
[features]
default = ["csr"]
default = []
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [

View File

@ -43,41 +43,39 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> Element {
pub fn Counters(cx: Scope) -> impl IntoView {
view! {
cx,
<div>
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" element=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" element=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
</div>
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" element=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" element=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
}
}
@ -86,7 +84,7 @@ pub fn Counters(cx: Scope) -> Element {
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter(cx: Scope) -> Element {
pub fn Counter(cx: Scope) -> impl IntoView {
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
let clear = create_action(cx, |_| clear_server_count());
@ -126,7 +124,7 @@ pub fn Counter(cx: Scope) -> Element {
// It uses the same invalidation pattern as the plain counter,
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter(cx: Scope) -> Element {
pub fn FormCounter(cx: Scope) -> impl IntoView {
let adjust = create_server_action::<AdjustServerCount>(cx);
let clear = create_server_action::<ClearServerCount>(cx);
@ -189,7 +187,7 @@ pub fn FormCounter(cx: Scope) -> Element {
// Whenever another user updates the value, it will update here
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> Element {
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
@ -198,7 +196,7 @@ pub fn MultiuserCounter(cx: Scope) -> Element {
let multiplayer_value = {
use futures::StreamExt;
let mut source = gloo::net::eventsource::futures::EventSource::new("/api/events")
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect_throw("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,

View File

@ -14,7 +14,7 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
mount_to_body(|cx| {
view! { cx, <Counters/> }
});
}

View File

@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
use std::{net::SocketAddr, env};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
@ -29,17 +30,20 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127,0,0,1],3000));
crate::counters::register_server_functions();
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_counter_isomorphic").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_counter_isomorphic", |cx| view! { cx, <Counters/> }))
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <Counters/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.bind(&addr)?
.run()
.await
}

View File

@ -1,4 +1,4 @@
use actix_web::*;
use actix_web::{web::Bytes, *};
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
@ -62,9 +62,12 @@ pub fn handle_server_fns() -> Route {
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
let mut res: HttpResponseBuilder;
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok()
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
@ -73,10 +76,23 @@ pub fn handle_server_fns() -> Route {
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
HttpResponse::SeeOther()
.insert_header(("Location", referer))
.content_type("application/json")
.body(serialized)
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type("application/x-www-form-urlencoded");
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
@ -103,32 +119,40 @@ pub fn handle_server_fns() -> Route {
/// ```
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// fn MyApp(cx: Scope) -> impl IntoView {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(|| {
///
/// let addr = SocketAddr::from(([127,0,0,1],3000));
/// HttpServer::new(move || {
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
/// render_options.write_to_file();
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream("leptos_example", |cx| view! { cx, <MyApp/> }))
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
/// })
/// .bind(("127.0.0.1", 8080))?
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn render_app_to_stream(
client_pkg_name: &'static str,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
) -> Route {
pub fn render_app_to_stream<IV>(
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where IV: IntoView
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
async move {
let path = req.path();
@ -148,30 +172,59 @@ pub fn render_app_to_stream(
provide_context(cx, MetaContext::new());
provide_context(cx, req.clone());
(app_fn)(cx)
(app_fn)(cx).into_view(cx)
}
};
let head = format!(r#"<!DOCTYPE html>
<html>
let pkg_path = &options.pkg_path;
let socket_ip = &options.socket_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match options.environment {
RustEnv::DEV => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
ws.onmessage = (ev) => {{
console.log(`Reload message: `);
if (ev.data === 'reload') window.location.reload();
}};
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
}})()
</script>
"#
),
RustEnv::PROD => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '/pkg/{client_pkg_name}.js'; init().then(hydrate);</script>"#);
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async move { head.clone() })
// TODO this leaks a runtime once per invocation
.chain(render_to_stream(move |cx| {
let app = app(cx);
.chain(render_to_stream_with_prefix(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
format!("{head}</head><body>").into()
}
))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
})

View File

@ -9,6 +9,7 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.18" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.18" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" }
@ -32,6 +33,7 @@ hydrate = [
"leptos_server/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_core/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",

View File

@ -126,6 +126,7 @@
//! # }
//! ```
pub use leptos_config::*;
pub use leptos_core::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};

11
leptos_config/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "leptos_config"
version = "0.0.18"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Configuraiton for the Leptos web framework."
[dependencies]
typed-builder = "0.11.0"

110
leptos_config/src/lib.rs Normal file
View File

@ -0,0 +1,110 @@
use std::{env::VarError, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
/// This struct serves as a convenient place to store details used for rendering.
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
/// to watch. It's also used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
/// of truth for render options
#[derive(TypedBuilder, Clone)]
pub struct RenderOptions {
/// The path and name of the WASM and JS files generated by wasm-bindgen
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
#[builder(setter(into))]
pub pkg_path: String,
/// Used to control whether the Websocket code for code watching is included.
/// I recommend passing in the result of `env::var("RUST_ENV")`
#[builder(setter(into), default)]
pub environment: RustEnv,
/// Provides a way to control the address leptos is served from.
/// Using an env variable here would allow you to run the same code in dev and prod
/// Defaults to `127.0.0.1:3000`
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub socket_address: SocketAddr,
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
/// Defaults to `3001`
#[builder(default = 3001)]
pub reload_port: u32,
}
impl RenderOptions {
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
pub fn write_to_file(&self) {
use std::fs;
let options = format!(
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
RenderOptions {{
pkg-path "{}"
environment "{:?}"
socket-address "{:?}"
reload-port {:?}
}}
"#,
self.pkg_path, self.environment, self.socket_address, self.reload_port
);
fs::write("./.leptos.kdl", options).expect("Unable to write file");
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
/// Defaults to PROD
#[derive(Debug, Clone)]
pub enum RustEnv {
PROD,
DEV,
}
impl Default for RustEnv {
fn default() -> Self {
Self::PROD
}
}
impl FromStr for RustEnv {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::PROD),
}
}
}
impl From<&str> for RustEnv {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for RustEnv {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::PROD,
}
}
}

View File

@ -10,6 +10,7 @@ description = "DOM operations for the Leptos web framework."
[dependencies]
cfg-if = "1"
educe = "0.4"
futures = "0.3"
gloo = "0.8"
html-escape = "0.2"
indexmap = "1.9"
@ -19,6 +20,7 @@ leptos_reactive = { path = "../leptos_reactive", default-features = false, versi
pad-adapter = "0.1"
paste = "1"
rustc-hash = "1.1.0"
serde_json = "1"
smallvec = "1"
tracing = "0.1"
typed-builder = "0.11"
@ -65,6 +67,7 @@ features = [
]
[features]
default = ["web"]
web = []
default = []
web = ["leptos_reactive/csr", "leptos/csr"]
ssr = ["leptos_reactive/ssr", "leptos/ssr"]
stable = ["leptos_reactive/stable"]

View File

@ -24,12 +24,15 @@ pub use components::*;
pub use events::typed as ev;
pub use helpers::*;
pub use html::*;
pub use js_sys;
use hydration::HydrationCtx;
use leptos_reactive::Scope;
pub use logging::*;
pub use node_ref::*;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use smallvec::SmallVec;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub use ssr::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::cell::LazyCell;
use std::{borrow::Cow, fmt};
@ -581,23 +584,6 @@ where
std::mem::forget(disposer);
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
/// Runs the given function and renders it's result to a string.
pub fn render_to_string<F, N>(f: F) -> String
where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
let runtime = leptos_reactive::create_runtime();
let view = leptos_reactive::run_scope(runtime, |cx| f(cx).into_view(cx));
HydrationCtx::reset_id();
runtime.dispose();
view.render_to_string().into_owned()
}
thread_local! {
pub(crate) static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();

View File

@ -1,13 +1,160 @@
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
use crate::{CoreComponent, HydrationCtx, View};
use crate::{CoreComponent, HydrationCtx, IntoView, View};
use cfg_if::cfg_if;
use futures::{Stream, StreamExt, stream::FuturesUnordered};
use itertools::Itertools;
use std::borrow::Cow;
use leptos_reactive::*;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
/// Renders the given function to a static HTML string.
///
/// ```
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// # use leptos_reactive::*; use leptos_dom::*; use leptos_macro::view;
/// let html = render_to_string(|cx| view! { cx,
/// <p>"Hello, world!"</p>
/// });
/// assert_eq!(html, r#"<p>Hello, world!</p>"#);
/// # }}
/// ```
pub fn render_to_string<F, N>(f: F) -> String
where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
let runtime = leptos_reactive::create_runtime();
let view = leptos_reactive::run_scope(runtime, |cx| f(cx).into_view(cx));
HydrationCtx::reset_id();
runtime.dispose();
view.render_to_string().into_owned()
}
/// Renders a function to a stream of HTML strings.
///
/// This renders:
/// 1) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 2) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream(view: impl FnOnce(Scope) -> View + 'static) -> impl Stream<Item = String> {
render_to_stream_with_prefix(view, |_| "".into())
}
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static
) -> impl Stream<Item = String> {
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
let ((shell, prefix, pending_resources, pending_fragments, serializers), _, disposer) =
run_scope_undisposed(runtime, {
move |cx| {
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx).render_to_string();
let resources = cx.all_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
let prefix = prefix(cx);
(
shell,
prefix,
pending_resources,
cx.pending_fragments(),
cx.serialization_resolvers(),
)
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, fut) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// resources and fragments
let resources_and_fragments = futures::stream::select(
// stream data for each Resource as it resolves
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}),
// stream HTML for each <Suspense/> as it resolves
fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
if(frag) frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
})
);
// HTML for the view function and script to store resources
futures::stream::once(async move {
format!(
r#"
{prefix}
{shell}
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
})
.chain(resources_and_fragments)
// dispose of Scope and Runtime
.chain(futures::stream::once(async move {
disposer.dispose();
runtime.dispose();
Default::default()
}))
}
impl View {
/// Consumes the node and renders it into an HTML string.
pub(crate) fn render_to_string(self) -> Cow<'static, str> {
pub fn render_to_string(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {

View File

@ -21,6 +21,7 @@ syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { path = "../leptos_dom", version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", version = "0.0.18" }
leptos_server = { path = "../leptos_server", version = "0.0.18" }
lazy_static = "1.4"
[dev-dependencies]

View File

@ -1,4 +1,5 @@
use cfg_if::cfg_if;
use leptos_server::Encoding;
use proc_macro2::{Literal, TokenStream as TokenStream2};
use quote::quote;
use syn::{
@ -26,9 +27,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
let ServerFnName {
struct_name,
prefix,
encoding,
..
} = syn::parse::<ServerFnName>(args)?;
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
let encoding = match encoding {
Encoding::Cbor => quote! { ::leptos::Encoding::Cbor },
Encoding::Url => quote! { ::leptos::Encoding::Url },
};
let body = syn::parse::<ServerFnBody>(s.into())?;
let fn_name = &body.ident;
@ -40,7 +46,10 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
if #[cfg(not(feature = "stable"))] {
use proc_macro::Span;
let span = Span::call_site();
#[cfg(not(target_os = "windows"))]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
#[cfg(target_os = "windows")]
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
} else {
let url = fn_name_as_str;
}
@ -135,6 +144,10 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#url
}
fn encoding() -> ::leptos::Encoding {
#encoding
}
#[cfg(feature = "ssr")]
fn call_fn(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
let #struct_name { #(#field_names),* } = self;
@ -157,7 +170,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
let prefix = #struct_name::prefix().to_string();
let url = prefix + "/" + #struct_name::url();
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }).await
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }, #encoding).await
}
})
}
@ -166,6 +179,8 @@ pub struct ServerFnName {
struct_name: Ident,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Encoding,
}
impl Parse for ServerFnName {
@ -173,11 +188,15 @@ impl Parse for ServerFnName {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse().unwrap_or(Encoding::Url);
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
})
}
}

View File

@ -75,15 +75,7 @@ impl RuntimeId {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
if let Some(runtime) = runtime {
for (scope_id, _) in runtime.scopes.borrow().iter() {
let scope = Scope {
runtime: self,
id: scope_id,
};
scope.dispose();
}
}
drop(runtime);
}
}
}

View File

@ -18,6 +18,12 @@ log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
thiserror = "1"
rmp-serde = "1.1.1"
serde_json = "1.0.89"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
proc-macro2 = "1.0.47"
ciborium = "0.2.0"
[dev-dependencies]
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }

View File

@ -27,8 +27,9 @@
//!
//! ### `#[server]`
//!
//! The `#[server]` macro allows you to annotate a function to indicate that it should only run
//! on the server (i.e., when you have an `ssr` feature in your crate that is enabled).
//! The [`#[server]` macro](leptos::leptos_macro::server) allows you to annotate a function to
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
//! crate that is enabled).
//!
//! ```rust,ignore
//! # use leptos_reactive::*;
@ -62,15 +63,23 @@
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
//! need to deserialize the result to return it to the client.
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/).
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
pub use form_urlencoded;
use leptos_reactive::*;
use proc_macro2::{Literal, TokenStream};
use quote::TokenStreamExt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{future::Future, pin::Pin};
use std::{future::Future, pin::Pin, str::FromStr};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
};
use thiserror::Error;
mod action;
@ -85,7 +94,7 @@ use std::{
};
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>>
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
+ Send
+ Sync;
@ -94,6 +103,17 @@ lazy_static::lazy_static! {
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, Arc<ServerFnTraitObj>>>> = Default::default();
}
/// A dual type to hold the possible Response datatypes
#[derive(Debug)]
pub enum Payload {
///Encodes Data using CBOR
Binary(Vec<u8>),
///Encodes data in the URL
Url(String),
///Encodes Data using Json
Json(String),
}
/// Attempts to find a server function registered at the given path.
///
/// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
@ -145,6 +165,54 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
.and_then(|fns| fns.get(path).cloned())
}
/// Holds the current options for encoding types.
/// More could be added, but they need to be serde
#[derive(Debug, PartialEq)]
pub enum Encoding {
/// A Binary Encoding Scheme Called Cbor
Cbor,
/// The Default URL-encoded encoding method
Url,
}
impl FromStr for Encoding {
type Err = ();
fn from_str(input: &str) -> Result<Encoding, Self::Err> {
match input {
"URL" => Ok(Encoding::Url),
"Cbor" => Ok(Encoding::Cbor),
_ => Err(()),
}
}
}
impl quote::ToTokens for Encoding {
fn to_tokens(&self, tokens: &mut TokenStream) {
let option: syn::Ident = match *self {
Encoding::Cbor => parse_quote!(Cbor),
Encoding::Url => parse_quote!(Url),
};
let expansion: syn::Ident = syn::parse_quote! {
Encoding::#option
};
tokens.append(expansion);
}
}
impl Parse for Encoding {
fn parse(input: ParseStream) -> syn::Result<Self> {
let variant_name: String = input.parse::<Literal>()?.to_string();
// Need doubled quotes because variant_name doubles it
match variant_name.as_ref() {
"\"Url\"" => Ok(Self::Url),
"\"Cbor\"" => Ok(Self::Cbor),
_ => panic!("Encoding Not Found"),
}
}
}
/// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
///
@ -162,7 +230,7 @@ where
Self: Serialize + DeserializeOwned + Sized + 'static,
{
/// The return type of the function.
type Output: Serializable;
type Output: Serialize;
/// URL prefix that should be prepended by the client to the generated URL.
fn prefix() -> &'static str;
@ -170,6 +238,9 @@ where
/// The path at which the server function can be reached on the server.
fn url() -> &'static str;
/// The path at which the server function can be reached on the server.
fn encoding() -> Encoding;
/// Runs the function on the server.
#[cfg(any(feature = "ssr", doc))]
fn call_fn(
@ -189,12 +260,20 @@ where
fn register() -> Result<(), ServerFnError> {
// create the handler for this server function
// takes a String -> returns its async value
let run_server_fn = Arc::new(|cx: Scope, data: &[u8]| {
// decode the args
let value = serde_urlencoded::from_bytes::<Self>(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
let value = match Self::encoding() {
Encoding::Url => serde_urlencoded::from_bytes(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string())),
Encoding::Cbor => {
println!("Deserialize Cbor!: {:x?}", &data);
ciborium::de::from_reader(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
};
Box::pin(async move {
let value = match value {
let value: Self = match value {
Ok(v) => v,
Err(e) => return Err(e),
};
@ -206,16 +285,26 @@ where
};
// serialize the output
let result = match result
.to_json()
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
Ok(r) => r,
Err(e) => return Err(e),
let result = match Self::encoding() {
Encoding::Url => match serde_json::to_string(&result)
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
Ok(r) => Payload::Url(r),
Err(e) => return Err(e),
},
Encoding::Cbor => {
let mut buffer: Vec<u8> = Vec::new();
match ciborium::ser::into_writer(&result, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))
{
Ok(_) => Payload::Binary(buffer),
Err(e) => return Err(e),
}
}
};
Ok(result)
}) as Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>>
}) as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
});
// store it in the hashmap
@ -256,20 +345,69 @@ pub enum ServerFnError {
/// Executes the HTTP call to call a server function from the client, given its URL and argument type.
#[cfg(not(feature = "ssr"))]
pub async fn call_server_fn<T>(url: &str, args: impl ServerFn) -> Result<T, ServerFnError>
pub async fn call_server_fn<T>(
url: &str,
args: impl ServerFn,
enc: Encoding,
) -> Result<T, ServerFnError>
where
T: Serializable + Sized,
T: serde::Serialize + serde::de::DeserializeOwned + Sized,
{
let args_form_data = serde_urlencoded::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
use ciborium::ser::into_writer;
use leptos_dom::js_sys::Uint8Array;
use serde_json::Deserializer as JSONDeserializer;
let resp = gloo_net::http::Request::post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.body(args_form_data)
.send()
.await
.map_err(|e| ServerFnError::Request(e.to_string()))?;
#[derive(Debug)]
enum Payload {
Binary(Vec<u8>),
Url(String),
}
// log!("ARGS TO ENCODE: {:#}", &args);
let args_encoded = match &enc {
Encoding::Url => Payload::Url(
serde_urlencoded::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
),
Encoding::Cbor => {
let mut buffer: Vec<u8> = Vec::new();
into_writer(&args, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Payload::Binary(buffer)
}
};
//log!("ENCODED DATA: {:#?}", args_encoded);
let content_type_header = match &enc {
Encoding::Url => "application/x-www-form-urlencoded",
Encoding::Cbor => "application/cbor",
};
let accept_header = match &enc {
Encoding::Url => "application/x-www-form-urlencoded",
Encoding::Cbor => "application/cbor",
};
let resp = match args_encoded {
Payload::Binary(b) => {
let slice_ref: &[u8] = &b;
let js_array = Uint8Array::from(slice_ref).buffer();
gloo_net::http::Request::post(url)
.header("Content-Type", content_type_header)
.header("Accept", accept_header)
.body(js_array)
.send()
.await
.map_err(|e| ServerFnError::Request(e.to_string()))?
}
Payload::Url(s) => gloo_net::http::Request::post(url)
.header("Content-Type", content_type_header)
.header("Accept", accept_header)
.body(s)
.send()
.await
.map_err(|e| ServerFnError::Request(e.to_string()))?,
};
// check for error status
let status = resp.status();
@ -277,10 +415,21 @@ where
return Err(ServerFnError::ServerError(resp.status_text()));
}
let text = resp
.text()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
if enc == Encoding::Cbor {
let binary = resp
.binary()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
T::from_json(&text).map_err(|e| ServerFnError::Deserialization(e.to_string()))
ciborium::de::from_reader(binary.as_slice())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
} else {
let text = resp
.text()
.await
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
let mut deserializer = JSONDeserializer::from_str(&text);
T::deserialize(&mut deserializer).map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}