From 920fcf728c5b6a22357ef5149ab6be8c09360ab7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 16:34:57 -0500 Subject: [PATCH 01/87] create onmounted event --- packages/html/Cargo.toml | 5 ++ packages/html/src/events.rs | 3 + packages/html/src/events/mounted.rs | 85 ++++++++++++++++++++++++ packages/html/src/web_sys_bind/events.rs | 49 +++++++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 packages/html/src/events/mounted.rs diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index e60fcd8e5..513cc253a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -39,6 +39,11 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", + "Element", + "DomRect", + "ScrollIntoViewOptions", + "ScrollLogicalPosition", + "ScrollBehavior", ] [dev-dependencies] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 853400c28..85a408d0b 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -33,6 +33,7 @@ mod form; mod image; mod keyboard; mod media; +mod mounted; mod mouse; mod pointer; mod scroll; @@ -51,6 +52,7 @@ pub use form::*; pub use image::*; pub use keyboard::*; pub use media::*; +pub use mounted::*; pub use mouse::*; pub use pointer::*; pub use scroll::*; @@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool { "animationiteration" => true, "transitionend" => true, "toggle" => true, + "mounted" => false, _ => true, } } diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs new file mode 100644 index 000000000..a5413bb5b --- /dev/null +++ b/packages/html/src/events/mounted.rs @@ -0,0 +1,85 @@ +//! Handles quering data from the renderer + +use euclid::Rect; + +use std::{any::Any, rc::Rc}; + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub trait RenderedElementBacking { + /// Get the renderer specific element for the given id + fn get_raw_element(&self) -> Option<&dyn Any> { + None + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + fn get_client_rect(&self) -> Option> { + None + } + + /// Scroll to make the element visible + fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { + None + } + + /// Set the focus on the element + fn set_focus(&self, _focus: bool) -> Option<()> { + None + } +} + +/// The way that scrolling should be performed +pub enum ScrollBehavior { + /// Scroll to the element immediately + Instant, + /// Scroll to the element smoothly + Smooth, +} + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub struct MountedData { + inner: Rc, +} + +impl MountedData { + /// Create a new MountedData + pub fn new(registry: impl RenderedElementBacking + 'static) -> Self { + Self { + inner: Rc::new(registry), + } + } + + /// Get the renderer specific element for the given id + pub fn get_raw_element(&self) -> Option<&dyn Any> { + self.inner.get_raw_element() + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + pub fn get_client_rect(&self) -> Option> { + self.inner.get_client_rect() + } + + /// Scroll to make the element visible + pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + self.inner.scroll_to(behavior) + } + + /// Set the focus on the element + pub fn set_focus(&self, focus: bool) -> Option<()> { + self.inner.set_focus(focus) + } +} + +use dioxus_core::Event; + +pub type MountedEvent = Event; + +impl_event! [ + MountedData; + + /// mounted + onmounted +]; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 196099444..01b25dfc2 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,14 +4,14 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::DragData; +use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; use wasm_bindgen::JsCast; use web_sys::{ - AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, - TransitionEvent, WheelEvent, + AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, + ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, }; macro_rules! uncheck_convert { @@ -193,3 +193,46 @@ impl From<&TransitionEvent> for TransitionData { } } } + +impl From<&web_sys::Element> for MountedData { + fn from(e: &web_sys::Element) -> Self { + MountedData::new(e.clone()) + } +} + +impl RenderedElementBacking for web_sys::Element { + fn get_client_rect(&self) -> Option> { + let rect = self.get_bounding_client_rect(); + Some(euclid::Rect::new( + euclid::Point2D::new(rect.left(), rect.top()), + euclid::Size2D::new(rect.width(), rect.height()), + )) + } + + fn get_raw_element(&self) -> Option<&dyn std::any::Any> { + Some(self) + } + + fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + match behavior { + ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), + ), + ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth), + ), + } + + Some(()) + } + + fn set_focus(&self, focus: bool) -> Option<()> { + self.dyn_ref::().and_then(|e| { + if focus { + e.focus().ok() + } else { + e.blur().ok() + } + }) + } +} From 1aad285853a4bbe340d9c4775fe8d1b7248a8ea2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 17:02:12 -0500 Subject: [PATCH 02/87] provide nicer error types --- packages/html/src/events/mounted.rs | 57 ++++++++++++++++++------ packages/html/src/web_sys_bind/events.rs | 44 +++++++++++------- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index a5413bb5b..dafbf46c8 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -2,30 +2,34 @@ use euclid::Rect; -use std::{any::Any, rc::Rc}; +use std::{ + any::Any, + fmt::{Display, Formatter}, + rc::Rc, +}; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. pub trait RenderedElementBacking { /// Get the renderer specific element for the given id - fn get_raw_element(&self) -> Option<&dyn Any> { - None + fn get_raw_element(&self) -> MountedResult<&dyn Any> { + Err(MountedError::NotSupported) } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> Option> { - None + fn get_client_rect(&self) -> MountedResult> { + Err(MountedError::NotSupported) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { - None + fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { + Err(MountedError::NotSupported) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> Option<()> { - None + fn set_focus(&self, _focus: bool) -> MountedResult<()> { + Err(MountedError::NotSupported) } } @@ -53,22 +57,22 @@ impl MountedData { } /// Get the renderer specific element for the given id - pub fn get_raw_element(&self) -> Option<&dyn Any> { + pub fn get_raw_element(&self) -> MountedResult<&dyn Any> { self.inner.get_raw_element() } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> Option> { + pub fn get_client_rect(&self) -> MountedResult> { self.inner.get_client_rect() } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { self.inner.scroll_to(behavior) } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> Option<()> { + pub fn set_focus(&self, focus: bool) -> MountedResult<()> { self.inner.set_focus(focus) } } @@ -83,3 +87,30 @@ impl_event! [ /// mounted onmounted ]; + +/// The MountedResult type for the MountedData +pub type MountedResult = Result; + +#[derive(Debug)] +/// The error type for the MountedData +pub enum MountedError { + /// The renderer does not support the requested operation + NotSupported, + /// The element was not found + OperationFailed(Box), +} + +impl Display for MountedError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + MountedError::NotSupported => { + write!(f, "The renderer does not support the requested operation") + } + MountedError::OperationFailed(e) => { + write!(f, "The operation failed: {}", e) + } + } + } +} + +impl std::error::Error for MountedError {} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 01b25dfc2..1983a31da 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,11 +4,13 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; +use crate::{ + DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior, +}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, @@ -201,19 +203,19 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> Option> { + fn get_client_rect(&self) -> MountedResult> { let rect = self.get_bounding_client_rect(); - Some(euclid::Rect::new( + Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), )) } - fn get_raw_element(&self) -> Option<&dyn std::any::Any> { - Some(self) + fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { + Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -223,16 +225,26 @@ impl RenderedElementBacking for web_sys::Element { ), } - Some(()) + Ok(()) } - fn set_focus(&self, focus: bool) -> Option<()> { - self.dyn_ref::().and_then(|e| { - if focus { - e.focus().ok() - } else { - e.blur().ok() - } - }) + fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.dyn_ref::() + .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) + .and_then(|e| { + (if focus { e.focus() } else { e.blur() }) + .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) + }) } } + +#[derive(Debug)] +struct FocusError(JsValue); + +impl std::fmt::Display for FocusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to focus element {:?}", self.0) + } +} + +impl std::error::Error for FocusError {} From cb5cb56ad352f7d4edc1fb53b96e24a9fcd5d795 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 18:28:34 -0500 Subject: [PATCH 03/87] implement onmounted for the web renderer --- .../interpreter/src/sledgehammer_bindings.rs | 7 +++ packages/web/src/dom.rs | 52 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 33f6b4420..650e721ef 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -116,6 +116,10 @@ mod js { export function set_node(id, node) { nodes[id] = node; } + export function get_node(id) { + console.log(nodes, id); + return nodes[id]; + } export function initilize(root, handler) { listeners.handler = handler; nodes = [root]; @@ -166,6 +170,9 @@ mod js { #[wasm_bindgen] pub fn set_node(id: u32, node: Node); + #[wasm_bindgen] + pub fn get_node(id: u32) -> Node; + #[wasm_bindgen] pub fn initilize(root: Node, handler: &Function); } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4d17749d8..8aae595eb 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,8 +10,8 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData}; -use dioxus_interpreter_js::{save_template, Channel}; +use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; +use dioxus_interpreter_js::{get_node, save_template, Channel}; use futures_channel::mpsc; use rustc_hash::FxHashMap; use std::{any::Any, rc::Rc}; @@ -27,6 +27,7 @@ pub struct WebsysDom { templates: FxHashMap, max_template_id: u32, pub(crate) interpreter: Channel, + event_channel: mpsc::UnboundedSender, } pub struct UiEvent { @@ -34,7 +35,6 @@ pub struct UiEvent { pub bubbles: bool, pub element: ElementId, pub data: Rc, - pub event: Event, } impl WebsysDom { @@ -48,8 +48,9 @@ impl WebsysDom { }; let interpreter = Channel::default(); - let handler: Closure = - Closure::wrap(Box::new(move |event: &web_sys::Event| { + let handler: Closure = Closure::wrap(Box::new({ + let event_channel = event_channel.clone(); + move |event: &web_sys::Event| { let name = event.type_(); let element = walk_event_for_id(event); let bubbles = dioxus_html::event_bubbles(name.as_str()); @@ -69,10 +70,10 @@ impl WebsysDom { bubbles, element, data, - event: event.clone(), }); } - })); + } + })); dioxus_interpreter_js::initilize( root.clone().unchecked_into(), @@ -85,6 +86,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + event_channel, } } @@ -156,6 +158,8 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; let i = &mut self.interpreter; + // we need to apply the mount events last, so we collect them here + let mut to_mount = Vec::new(); for edit in &edits { match edit { AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), @@ -206,17 +210,43 @@ impl WebsysDom { }, SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); - } - RemoveEventListener { name, id } => { - i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + match *name { + // mounted events are fired immediately after the element is mounted. + "mounted" => { + to_mount.push(*id); + } + _ => { + i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + } } + RemoveEventListener { name, id } => match *name { + "mounted" => {} + _ => { + i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + }, Remove { id } => i.remove(id.0 as u32), PushRoot { id } => i.push_root(id.0 as u32), } } edits.clear(); i.flush(); + + for id in to_mount { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + log::info!("mounted event fired: {}", id.0); + let data: MountedData = element.into(); + let data = Rc::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data, + }); + } + } } } From 7636c046faa7b94f6ddca727a2ecdfd5511888c3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Mar 2023 16:10:34 -0500 Subject: [PATCH 04/87] implement on mounted for desktop --- packages/desktop/src/desktop_context.rs | 5 + packages/desktop/src/element.rs | 208 +++++++++++++++++++++++ packages/desktop/src/lib.rs | 60 ++++++- packages/html/src/events/mounted.rs | 36 ++-- packages/html/src/transit.rs | 52 ++++++ packages/html/src/web_sys_bind/events.rs | 26 ++- packages/interpreter/Cargo.toml | 2 + packages/interpreter/src/interpreter.js | 85 +++++++++ 8 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 packages/desktop/src/element.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2ed4bbee9..ccb8c7588 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; +use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; use crate::shortcut::IntoKeyCode; @@ -62,6 +63,9 @@ pub struct DesktopContext { /// The receiver for eval results since eval is async pub(super) eval: tokio::sync::broadcast::Sender, + /// The receiver for queries about elements + pub(super) query: QueryEngine, + pub(super) pending_windows: WebviewQueue, pub(crate) event_loop: EventLoopWindowTarget, @@ -97,6 +101,7 @@ impl DesktopContext { proxy, event_loop, eval: tokio::sync::broadcast::channel(8).0, + query: Default::default(), pending_windows: webviews, event_handlers, shortcut_manager, diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs new file mode 100644 index 000000000..60a995f1b --- /dev/null +++ b/packages/desktop/src/element.rs @@ -0,0 +1,208 @@ +use std::{cell::RefCell, rc::Rc}; + +use dioxus_core::ElementId; +use dioxus_html::{ + MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, + RenderedElementBacking, +}; +use slab::Slab; +use wry::webview::WebView; + +/// A mounted element passed to onmounted events +pub struct DesktopElement { + id: ElementId, + webview: Rc, + query: QueryEngine, +} + +impl DesktopElement { + pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { + Self { id, webview, query } + } + + /// Get the id of the element + pub fn id(&self) -> ElementId { + self.id + } + + /// Get the webview the element is mounted in + pub fn webview(&self) -> &Rc { + &self.webview + } +} + +impl RenderedElementBacking for DesktopElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let fut = self + .query + .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query( + self.id, + NodeUpdateData::ScrollTo { behavior }, + &self.webview, + ) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::ScrollTo(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::SetFocus(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } +} + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + let update = NodeUpdate { + id: id.0 as u32, + request_id, + data: update, + }; + + // start the query + webview + .evaluate_script(&format!( + "window.interpreter.handleNodeUpdate({})", + serde_json::to_string(&update).unwrap() + )) + .unwrap(); + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + } + } + + pub fn send(&self, data: MountedReturn) { + self.sender.send(data).unwrap(); + } +} + +struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, +} + +impl Query { + async fn resolve(mut self) -> Option { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break result.data; + } + } + Err(_) => { + break None; + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, + MismatchedReturn, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + DesktopQueryError::MismatchedReturn => { + write!(f, "The return type did not match the query") + } + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 75c67dd22..079331fdb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,6 +5,7 @@ mod cfg; mod desktop_context; +mod element; mod escape; mod eval; mod events; @@ -19,7 +20,8 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::HtmlEvent; +use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; @@ -220,19 +222,69 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } EventData::Ipc(msg) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { + let params = msg.params(); + + let evt = match serde_json::from_value::(params) { Ok(value) => value, Err(_) => return, }; + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + let view = webviews.get_mut(&event.1).unwrap(); - view.dom - .handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + // check for a mounted event placeholder and replace it with a desktop specific element + let as_any = if let dioxus_html::EventData::Mounted = &data { + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + let element = DesktopElement::new(element, view.webview.clone(), query); + + Rc::new(MountedData::new(element)) + } else { + data.into_any() + }; + + view.dom.handle_event(&name, as_any, element, bubbles); send_edits(view.dom.render_immediate(), &view.webview); } + EventData::Ipc(msg) if msg.method() == "node_update" => { + let params = msg.params(); + println!("node_update: {:?}", params); + + // check for a mounted event + let evt = match serde_json::from_value::(params) { + Ok(value) => value, + Err(err) => { + println!("node_update: {:?}", err); + return; + } + }; + + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + println!("node_update: {:?}", evt); + + query.send(evt); + } + EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); send_edits(view.dom.rebuild(), &view.webview); diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index dafbf46c8..548359b5c 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -5,12 +5,15 @@ use euclid::Rect; use std::{ any::Any, fmt::{Display, Formatter}, + future::Future, + pin::Pin, rc::Rc, }; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +// we can not use async_trait here because it does not create a trait that is object safe pub trait RenderedElementBacking { /// Get the renderer specific element for the given id fn get_raw_element(&self) -> MountedResult<&dyn Any> { @@ -18,26 +21,35 @@ pub trait RenderedElementBacking { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> MountedResult> { - Err(MountedError::NotSupported) + #[allow(clippy::type_complexity)] + fn get_client_rect(&self) -> Pin>>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn scroll_to( + &self, + _behavior: ScrollBehavior, + ) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn set_focus(&self, _focus: bool) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } } +impl RenderedElementBacking for () {} + /// The way that scrolling should be performed +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum ScrollBehavior { /// Scroll to the element immediately + #[cfg_attr(feature = "serialize", serde(rename = "instant"))] Instant, /// Scroll to the element smoothly + #[cfg_attr(feature = "serialize", serde(rename = "smooth"))] Smooth, } @@ -62,18 +74,18 @@ impl MountedData { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> MountedResult> { - self.inner.get_client_rect() + pub async fn get_client_rect(&self) -> MountedResult> { + self.inner.get_client_rect().await } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior) + pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + self.inner.scroll_to(behavior).await } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus) + pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.inner.set_focus(focus).await } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 651b7e4f6..12c35bd28 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,6 +2,7 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; +use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -113,6 +114,9 @@ fn fun_name( // Toggle "toggle" => Toggle(de(data)?), + // Mounted + "mounted" => Mounted, + // ImageData => "load" | "error"; // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; other => { @@ -151,6 +155,7 @@ pub enum EventData { Animation(AnimationData), Transition(TransitionData), Toggle(ToggleData), + Mounted, } impl EventData { @@ -172,6 +177,7 @@ impl EventData { EventData::Animation(data) => Rc::new(data) as Rc, EventData::Transition(data) => Rc::new(data) as Rc, EventData::Toggle(data) => Rc::new(data) as Rc, + EventData::Mounted => Rc::new(MountedData::new(())) as Rc, } } } @@ -215,3 +221,49 @@ fn test_back_and_forth() { assert_eq!(data, p); } + +/// Message to update a node to support MountedData +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct NodeUpdate { + /// The id of the node to update + pub id: u32, + /// The id of the request + pub request_id: usize, + /// The data to update the node with + pub data: NodeUpdateData, +} + +/// Message to update a node to support MountedData +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum NodeUpdateData { + SetFocus { focus: bool }, + GetClientRect {}, + ScrollTo { behavior: ScrollBehavior }, +} + +/// The result of a element query +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct MountedReturn { + /// A unique id for the query + pub id: usize, + /// The result of the query + pub data: Option, +} + +/// The data of a element query +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum MountedReturnData { + SetFocus(()), + GetClientRect(Rect), + ScrollTo(()), +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 1983a31da..bcbe80dda 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -9,6 +9,8 @@ use crate::{ }; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ @@ -203,19 +205,25 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> MountedResult> { + fn get_client_rect( + &self, + ) -> Pin>>>> { let rect = self.get_bounding_client_rect(); - Ok(euclid::Rect::new( + let result = Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), - )) + )); + Box::pin(async { result }) } fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -225,16 +233,18 @@ impl RenderedElementBacking for web_sys::Element { ), } - Ok(()) + Box::pin(async { Ok(()) }) } - fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.dyn_ref::() + fn set_focus(&self, focus: bool) -> Pin>>> { + let result = self + .dyn_ref::() .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) .and_then(|e| { (if focus { e.focus() } else { e.blur() }) .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) - }) + }); + Box::pin(async { result }) } } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7c7d79607..bb8f7ec60 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -19,8 +19,10 @@ js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } sledgehammer_bindgen = { version = "0.1.3", optional = true } sledgehammer_utils = { version = "0.1.0", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] +serialize = ["serde"] web = ["wasm-bindgen", "js-sys", "web-sys"] sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b921c7c22..ce2a010c0 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -204,6 +204,76 @@ class Interpreter { node.removeAttribute(name); } } + + GetClientRect(id) { + const node= this.nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [ + rect.x, + rect.y, + ], + size: [ + rect.width, + rect.height, + ] + }; + } + + ScrollTo(id, behavior) { + const node = this.nodes[id]; + if (!node) { + return; + } + node.scrollIntoView({ + behavior: behavior + }); + return { + type: "ScrollTo", + }; + } + + /// Set the focus on the element + SetFocus(id, focus) { + const node = this.nodes[id]; + if (!node) { + return; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return { + type: "SetFocus", + }; + } + + handleNodeUpdate(edit) { + let data; + switch (edit.data.type) { + case "SetFocus": + data = this.SetFocus(edit.id, edit.data.focus); + break; + case "ScrollTo": + data = this.ScrollTo(edit.id, edit.data.behavior); + break; + case "GetClientRect": + data = this.GetClientRect(edit.id); + break; + } + window.ipc.postMessage( + serializeIpcMessage("node_update", { + id: edit.request_id, + data: data + }) + ); + } + handleEdits(edits) { for (let template of edits.templates) { this.SaveTemplate(template); @@ -345,6 +415,19 @@ class Interpreter { let bubbles = event_bubbles(edit.name); + // if this is a mounted listener, we send the event immediately + if (edit.name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: edit.id, + data: null, + bubbles, + }) + ); + } + + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { @@ -921,6 +1004,8 @@ function event_bubbles(event) { return true; case "toggle": return true; + case "mounted": + return false; } return true; From fa9f0d0f6ca6037c687c99afcf6dfd8163d89d4b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 15:19:00 -0500 Subject: [PATCH 05/87] Generalize Query system for use in use_eval and node querys --- packages/desktop/src/desktop_context.rs | 33 +---- packages/desktop/src/element.rs | 165 ++++++------------------ packages/desktop/src/eval.rs | 22 ++-- packages/desktop/src/lib.rs | 52 +++----- packages/desktop/src/query.rs | 105 +++++++++++++++ packages/html/src/events/mounted.rs | 11 +- packages/html/src/transit.rs | 47 ------- packages/interpreter/src/interpreter.js | 33 +---- 8 files changed, 186 insertions(+), 282 deletions(-) create mode 100644 packages/desktop/src/query.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ccb8c7588..6af6401f3 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,9 +3,9 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; -use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::query::QueryEngine; use crate::shortcut::IntoKeyCode; use crate::shortcut::IntoModifersState; use crate::shortcut::ShortcutId; @@ -17,7 +17,6 @@ use dioxus_core::ScopeState; use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; -use serde_json::Value; use slab::Slab; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; @@ -60,10 +59,7 @@ pub struct DesktopContext { /// The proxy to the event loop pub proxy: ProxyType, - /// The receiver for eval results since eval is async - pub(super) eval: tokio::sync::broadcast::Sender, - - /// The receiver for queries about elements + /// The receiver for queries about the current window pub(super) query: QueryEngine, pub(super) pending_windows: WebviewQueue, @@ -100,7 +96,6 @@ impl DesktopContext { webview, proxy, event_loop, - eval: tokio::sync::broadcast::channel(8).0, query: Default::default(), pending_windows: webviews, event_handlers, @@ -215,28 +210,10 @@ impl DesktopContext { /// Evaluate a javascript expression pub fn eval(&self, code: &str) -> EvalResult { - // Embed the return of the eval in a function so we can send it back to the main thread - let script = format!( - r#" - window.ipc.postMessage( - JSON.stringify({{ - "method":"eval_result", - "params": ( - function(){{ - {code} - }} - )() - }}) - ); - "# - ); + // the query id lets us keep track of the eval result and send it back to the main thread + let query = self.query.new_query(code, &self.webview); - if let Err(e) = self.webview.evaluate_script(&script) { - // send an error to the eval receiver - log::warn!("Eval script error: {e}"); - } - - EvalResult::new(self.eval.clone()) + EvalResult::new(query) } /// Create a wry event handler that listens for wry events. diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index 60a995f1b..e94a481c7 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -1,13 +1,11 @@ -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; use dioxus_core::ElementId; -use dioxus_html::{ - MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, - RenderedElementBacking, -}; -use slab::Slab; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; use wry::webview::WebView; +use crate::query::QueryEngine; + /// A mounted element passed to onmounted events pub struct DesktopElement { id: ElementId, @@ -19,16 +17,6 @@ impl DesktopElement { pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { Self { id, webview, query } } - - /// Get the id of the element - pub fn id(&self) -> ElementId { - self.id - } - - /// Get the webview the element is mounted in - pub fn webview(&self) -> &Rc { - &self.webview - } } impl RenderedElementBacking for DesktopElement { @@ -45,19 +33,21 @@ impl RenderedElementBacking for DesktopElement { >, >, > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let fut = self .query - .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .new_query::>>(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -66,23 +56,25 @@ impl RenderedElementBacking for DesktopElement { &self, behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + let fut = self .query - .new_query( - self.id, - NodeUpdateData::ScrollTo { behavior }, - &self.webview, - ) + .new_query::(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::ScrollTo(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -91,116 +83,41 @@ impl RenderedElementBacking for DesktopElement { &self, focus: bool, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + println!("script: {}", script); let fut = self .query - .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .new_query::(&script, &self.webview) .resolve(); + println!("fut"); + Box::pin(async move { match fut.await { - Some(MountedReturnData::SetFocus(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } } -#[derive(Default, Clone)] -struct SharedSlab { - slab: Rc>>, -} - -#[derive(Clone)] -pub(crate) struct QueryEngine { - sender: Rc>, - active_requests: SharedSlab, -} - -impl Default for QueryEngine { - fn default() -> Self { - let (sender, _) = tokio::sync::broadcast::channel(8); - Self { - sender: Rc::new(sender), - active_requests: SharedSlab::default(), - } - } -} - -impl QueryEngine { - fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { - let request_id = self.active_requests.slab.borrow_mut().insert(()); - - let update = NodeUpdate { - id: id.0 as u32, - request_id, - data: update, - }; - - // start the query - webview - .evaluate_script(&format!( - "window.interpreter.handleNodeUpdate({})", - serde_json::to_string(&update).unwrap() - )) - .unwrap(); - - Query { - slab: self.active_requests.clone(), - id: request_id, - reciever: self.sender.subscribe(), - } - } - - pub fn send(&self, data: MountedReturn) { - self.sender.send(data).unwrap(); - } -} - -struct Query { - slab: SharedSlab, - id: usize, - reciever: tokio::sync::broadcast::Receiver, -} - -impl Query { - async fn resolve(mut self) -> Option { - let result = loop { - match self.reciever.recv().await { - Ok(result) => { - if result.id == self.id { - break result.data; - } - } - Err(_) => { - break None; - } - } - }; - - // Remove the query from the slab - self.slab.slab.borrow_mut().remove(self.id); - - result - } -} - #[derive(Debug)] enum DesktopQueryError { FailedToQuery, - MismatchedReturn, } impl std::fmt::Display for DesktopQueryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), - DesktopQueryError::MismatchedReturn => { - write!(f, "The return type did not match the query") - } } } } diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 7a92bedf5..1e11cad18 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,36 +1,32 @@ use std::rc::Rc; +use crate::query::Query; +use crate::query::QueryError; use crate::use_window; use dioxus_core::ScopeState; -use serde::de::Error; use std::future::Future; use std::future::IntoFuture; use std::pin::Pin; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { - pub(crate) broadcast: tokio::sync::broadcast::Sender, + pub(crate) query: Query, } impl EvalResult { - pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { - Self { broadcast: sender } + pub(crate) fn new(query: Query) -> Self { + Self { query } } } impl IntoFuture for EvalResult { - type Output = Result; + type Output = Result; - type IntoFuture = Pin>>>; + type IntoFuture = Pin>>>; fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.broadcast.subscribe(); - match reciever.recv().await { - Ok(result) => Ok(result), - Err(_) => Err(serde_json::Error::custom("No result returned")), - } - }) as Pin>>> + Box::pin(self.query.resolve()) + as Pin>>> } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 079331fdb..8e92d90aa 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -10,17 +10,19 @@ mod escape; mod eval; mod events; mod protocol; +mod query; mod shortcut; mod waker; mod webview; +use crate::query::QueryResult; pub use cfg::Config; pub use desktop_context::{ use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use dioxus_html::{HtmlEvent, MountedData}; use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; @@ -259,30 +261,21 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.render_immediate(), &view.webview); } - EventData::Ipc(msg) if msg.method() == "node_update" => { + // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query + EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - println!("node_update: {:?}", params); - // check for a mounted event - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - println!("node_update: {:?}", err); - return; - } - }; + if let Ok(result) = dbg!(serde_json::from_value::(params)) { + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query; - - println!("node_update: {:?}", evt); - - query.send(evt); + query.send(result); + } } EventData::Ipc(msg) if msg.method() == "initialize" => { @@ -290,21 +283,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.rebuild(), &view.webview); } - // When the webview chirps back with the result of the eval, we send it to the active receiver - // - // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, - // you might the wrong result. This should be fixed - EventData::Ipc(msg) if msg.method() == "eval_result" => { - webviews[&event.1] - .dom - .base_scope() - .consume_context::() - .unwrap() - .eval - .send(msg.params()) - .unwrap(); - } - EventData::Ipc(msg) if msg.method() == "browser_open" => { if let Some(temp) = msg.params().as_object() { if temp.contains_key("href") { diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs new file mode 100644 index 000000000..8b0cf3fc3 --- /dev/null +++ b/packages/desktop/src/query.rs @@ -0,0 +1,105 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::broadcast::error::RecvError; +use wry::webview::WebView; + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + pub fn new_query(&self, script: &str, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = webview.evaluate_script(&format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index 548359b5c..293b51b9e 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -79,13 +79,16 @@ impl MountedData { } /// Scroll to make the element visible - pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior).await + pub fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { + self.inner.scroll_to(behavior) } /// Set the focus on the element - pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus).await + pub fn set_focus(&self, focus: bool) -> Pin>>> { + self.inner.set_focus(focus) } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 12c35bd28..ef0e7c70d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,7 +2,6 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; -use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -221,49 +220,3 @@ fn test_back_and_forth() { assert_eq!(data, p); } - -/// Message to update a node to support MountedData -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct NodeUpdate { - /// The id of the node to update - pub id: u32, - /// The id of the request - pub request_id: usize, - /// The data to update the node with - pub data: NodeUpdateData, -} - -/// Message to update a node to support MountedData -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum NodeUpdateData { - SetFocus { focus: bool }, - GetClientRect {}, - ScrollTo { behavior: ScrollBehavior }, -} - -/// The result of a element query -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct MountedReturn { - /// A unique id for the query - pub id: usize, - /// The result of the query - pub data: Option, -} - -/// The data of a element query -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum MountedReturnData { - SetFocus(()), - GetClientRect(Rect), - ScrollTo(()), -} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index ce2a010c0..2c919dcde 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -227,51 +227,26 @@ class Interpreter { ScrollTo(id, behavior) { const node = this.nodes[id]; if (!node) { - return; + return false; } node.scrollIntoView({ behavior: behavior }); - return { - type: "ScrollTo", - }; + return true; } /// Set the focus on the element SetFocus(id, focus) { const node = this.nodes[id]; if (!node) { - return; + return false; } if (focus) { node.focus(); } else { node.blur(); } - return { - type: "SetFocus", - }; - } - - handleNodeUpdate(edit) { - let data; - switch (edit.data.type) { - case "SetFocus": - data = this.SetFocus(edit.id, edit.data.focus); - break; - case "ScrollTo": - data = this.ScrollTo(edit.id, edit.data.behavior); - break; - case "GetClientRect": - data = this.GetClientRect(edit.id); - break; - } - window.ipc.postMessage( - serializeIpcMessage("node_update", { - id: edit.request_id, - data: data - }) - ); + return true; } handleEdits(edits) { From 8243dfe00dceff7c0ee49d5312d1cbf5a15a40b6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 16:52:29 -0500 Subject: [PATCH 06/87] implement a query engine for liveview --- packages/desktop/src/query.rs | 5 ++ packages/liveview/Cargo.toml | 2 + packages/liveview/src/element.rs | 125 +++++++++++++++++++++++++++++++ packages/liveview/src/lib.rs | 2 + packages/liveview/src/main.js | 16 +++- packages/liveview/src/pool.rs | 76 ++++++++++++++++--- packages/liveview/src/query.rs | 113 ++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 packages/liveview/src/element.rs create mode 100644 packages/liveview/src/query.rs diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 8b0cf3fc3..bd8b2ee2a 100644 --- a/packages/desktop/src/query.rs +++ b/packages/desktop/src/query.rs @@ -7,11 +7,13 @@ use thiserror::Error; use tokio::sync::broadcast::error::RecvError; use wry::webview::WebView; +/// Tracks what query ids are currently active #[derive(Default, Clone)] struct SharedSlab { slab: Rc>>, } +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. #[derive(Clone)] pub(crate) struct QueryEngine { sender: Rc>, @@ -29,6 +31,7 @@ impl Default for QueryEngine { } impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. pub fn new_query(&self, script: &str, webview: &WebView) -> Query { let request_id = self.active_requests.slab.borrow_mut().insert(()); @@ -56,6 +59,7 @@ impl QueryEngine { } } + /// Send a query result pub fn send(&self, data: QueryResult) { let _ = self.sender.send(data); } @@ -69,6 +73,7 @@ pub(crate) struct Query { } impl Query { + /// Resolve the query pub async fn resolve(mut self) -> Result { let result = loop { match self.reciever.recv().await { diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 5269927b0..8b80fedb8 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -14,6 +14,8 @@ license = "MIT/Apache-2.0" [dependencies] thiserror = "1.0.38" +log = "0.4.14" +slab = "0.4" futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs new file mode 100644 index 000000000..21be23bee --- /dev/null +++ b/packages/liveview/src/element.rs @@ -0,0 +1,125 @@ +use dioxus_core::ElementId; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::query::QueryEngine; + +/// A mounted element passed to onmounted events +pub struct LiveviewElement { + id: ElementId, + query_tx: UnboundedSender, + query: QueryEngine, +} + +impl LiveviewElement { + pub(crate) fn new(id: ElementId, tx: UnboundedSender, query: QueryEngine) -> Self { + Self { + id, + query_tx: tx, + query, + } + } +} + +impl RenderedElementBacking for LiveviewElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + + let fut = self + .query + .new_query::>>(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index ffddf0272..0c6fc9b2a 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,7 +18,9 @@ pub mod adapters { pub use adapters::*; +mod element; pub mod pool; +mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 7dd9758d6..594c764c2 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -26,11 +26,19 @@ class IPC { // todo: retry the connection }; - ws.onmessage = (event) => { + ws.onmessage = (message) => { // Ignore pongs - if (event.data != "__pong__") { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); + if (message.data != "__pong__") { + const event = JSON.parse(message.data); + switch (event.type) { + case "edits": + let edits = event.data; + window.interpreter.handleEdits(edits); + break; + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } }; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index e02c8195b..089d0bad4 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,8 +1,13 @@ -use crate::LiveViewError; -use dioxus_core::prelude::*; -use dioxus_html::HtmlEvent; +use crate::{ + element::LiveviewElement, + query::{QueryEngine, QueryResult}, + LiveViewError, +}; +use dioxus_core::{prelude::*, Mutations}; +use dioxus_html::{EventData, HtmlEvent, MountedData}; use futures_util::{pin_mut, SinkExt, StreamExt}; -use std::time::Duration; +use serde::Serialize; +use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li }; // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); // pin the futures so we can use select! pin_mut!(ws); @@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // send the initial render to the client ws.send(edits).await?; + // Create the a proxy for query engine + let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); + let query_engine = QueryEngine::default(); + // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry - #[derive(serde::Deserialize)] - struct IpcMessage { - params: HtmlEvent, + #[derive(serde::Deserialize, Debug)] + #[serde(tag = "method", content = "params")] + enum IpcMessage { + #[serde(rename = "user_event")] + Event(HtmlEvent), + #[serde(rename = "query")] + Query(QueryResult), } loop { @@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li ws.send("__pong__".to_string()).await?; } Some(Ok(evt)) => { - if let Ok(IpcMessage { params }) = serde_json::from_str::(evt) { - vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + if let Ok(message) = serde_json::from_str::(evt) { + match message { + IpcMessage::Event(evt) => { + // Intercept the mounted event and insert a custom element type + if let EventData::Mounted = &evt.data { + let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone()); + vdom.handle_event( + &evt.name, + Rc::new(MountedData::new(element)), + evt.element, + evt.bubbles, + ); + } + else{ + vdom.handle_event( + &evt.name, + evt.data.into_any(), + evt.element, + evt.bubbles, + ); + } + } + IpcMessage::Query(result) => { + query_engine.send(result); + }, + } } } // log this I guess? when would we get an error here? - Some(Err(_e)) => {}, + Some(Err(_e)) => {} None => return Ok(()), } } + // handle any new queries + Some(query) = query_rx.recv() => { + ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?; + } + Some(msg) = hot_reload_wait => { #[cfg(all(feature = "hot-reload", debug_assertions))] match msg{ @@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send(serde_json::to_string(&edits).unwrap()).await?; + ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap()) + .await?; } } + +#[derive(Serialize)] +#[serde(tag = "type", content = "data")] +enum ClientUpdate<'a> { + #[serde(rename = "edits")] + Edits(Mutations<'a>), + #[serde(rename = "query")] + Query(String), +} diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs new file mode 100644 index 000000000..04d531a63 --- /dev/null +++ b/packages/liveview/src/query.rs @@ -0,0 +1,113 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender}; + +/// Tracks what query ids are currently active +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. + pub fn new_query( + &self, + script: &str, + tx: &UnboundedSender, + ) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = tx.send(format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + /// Send a query result + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + /// Resolve the query + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} From e0d46d982092074304693917e73cbb3ea0202131 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 17:33:27 -0500 Subject: [PATCH 07/87] implement mounted event for TUI --- packages/tui/src/element.rs | 92 +++++++++++++++++++++++++++++++++++++ packages/tui/src/lib.rs | 10 ++++ 2 files changed, 102 insertions(+) create mode 100644 packages/tui/src/element.rs diff --git a/packages/tui/src/element.rs b/packages/tui/src/element.rs new file mode 100644 index 000000000..5ea1e56af --- /dev/null +++ b/packages/tui/src/element.rs @@ -0,0 +1,92 @@ +use std::{ + fmt::{Display, Formatter}, + rc::Rc, +}; + +use dioxus_core::{ElementId, Mutations, VirtualDom}; +use dioxus_html::{ + geometry::euclid::{Point2D, Rect, Size2D}, + MountedData, MountedError, RenderedElementBacking, +}; + +use crate::query::{ElementRef, Query}; + +pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec { + let mut mount_events = Vec::new(); + for mutation in &mutations.edits { + if let dioxus_core::Mutation::NewEventListener { + name: "mounted", + id, + } = mutation + { + mount_events.push(*id); + } + } + mount_events +} + +pub(crate) fn send_mounted_events(vdom: &mut VirtualDom, mount_events: Vec) { + let query: Query = vdom + .base_scope() + .consume_context() + .expect("Query should be in context"); + for id in mount_events { + let element = TuiElement { + query: query.clone(), + id, + }; + vdom.handle_event("mounted", Rc::new(MountedData::new(element)), id, false); + } +} + +struct TuiElement { + query: Query, + id: ElementId, +} + +impl TuiElement { + pub(crate) fn element(&self) -> ElementRef { + self.query.get(self.id) + } +} + +impl RenderedElementBacking for TuiElement { + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let layout = self.element().layout(); + Box::pin(async move { + match layout { + Some(layout) => { + let x = layout.location.x as f64; + let y = layout.location.y as f64; + let width = layout.size.width as f64; + let height = layout.size.height as f64; + Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height))) + } + None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))), + } + }) + } + + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } +} + +#[derive(Debug)] +struct TuiElementNotFound; + +impl Display for TuiElementNotFound { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "TUI element not found") + } +} + +impl std::error::Error for TuiElementNotFound {} diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 75c865de5..911c7c4db 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,6 +7,7 @@ use crossterm::{ }; use dioxus_core::*; use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, NodeMask, SendAnyMap}; +use element::{find_mount_events, send_mounted_events}; use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, @@ -26,6 +27,7 @@ use tokio::select; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod element; mod focus; mod hooks; mod layout; @@ -114,7 +116,11 @@ pub fn launch_cfg_with_props(app: Component, props: Props { let mut rdom = rdom.borrow_mut(); let mutations = dom.rebuild(); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); let (to_update, _) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(&mut dom, mounted); let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); let _to_rerender = rdom.update_state(to_update, any_map); @@ -307,8 +313,12 @@ fn render_vdom( let mut rdom = rdom.borrow_mut(); let mutations = vdom.render_immediate(); handler.prune(&mutations, &rdom); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); // updates the dom's nodes let (to_update, dirty) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(vdom, mounted); // update the style and layout let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); From a551c0fcb8e4f5481c49d6d2b4a0ab2ab18081be Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 11:32:42 -0500 Subject: [PATCH 08/87] add examples for onmounted --- examples/controll_focus.rs | 44 +++++++++++++++++++++++++ examples/read_size.rs | 57 +++++++++++++++++++++++++++++++++ examples/scroll_to_top.rs | 33 +++++++++++++++++++ packages/desktop/src/element.rs | 2 -- packages/desktop/src/lib.rs | 2 +- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 examples/controll_focus.rs create mode 100644 examples/read_size.rs create mode 100644 examples/scroll_to_top.rs diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs new file mode 100644 index 000000000..0f14a940b --- /dev/null +++ b/examples/controll_focus.rs @@ -0,0 +1,44 @@ +use std::rc::Rc; + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let running = use_state(cx, || true); + + use_future!(cx, |(elements, running)| async move { + let mut focused = 0; + if *running.current() { + loop { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + if let Some(element) = elements.read().get(focused) { + element.set_focus(true); + } else { + focused = 0; + } + focused += 1; + } + } + }); + + cx.render(rsx!( + div { + h1 { "Input Roulette" } + for i in 0..100 { + input { + value: "{i}", + onmounted: move |cx| { + elements.write().push(cx.inner().clone()); + }, + oninput: move |_| { + running.set(false); + } + } + } + } + )) +} diff --git a/examples/read_size.rs b/examples/read_size.rs new file mode 100644 index 000000000..be996e2dd --- /dev/null +++ b/examples/read_size.rs @@ -0,0 +1,57 @@ +use std::rc::Rc; + +use dioxus::{html::geometry::euclid::Rect, prelude::*}; + +fn main() { + dioxus_desktop::launch_cfg( + app, + dioxus_desktop::Config::default().with_custom_head( + r#" + +"# + .to_owned(), + ), + ); +} + +fn app(cx: Scope) -> Element { + let div_element: &UseRef>> = use_ref(cx, || None); + + let dimentions = use_ref(cx, || Rect::zero()); + + cx.render(rsx!( + div { + width: "50%", + height: "50%", + background_color: "red", + onmounted: move |cx| { + div_element.set(Some(cx.inner().clone())); + }, + "This element is {dimentions.read():?}" + } + + button { + onclick: move |_| { + to_owned![div_element, dimentions]; + async move { + if let Some(div) = div_element.read().as_ref() { + if let Ok(rect)=div.get_client_rect().await{ + dimentions.set(rect); + } + } + } + }, + "Read dimentions" + } + )) +} diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs new file mode 100644 index 000000000..4eb38a451 --- /dev/null +++ b/examples/scroll_to_top.rs @@ -0,0 +1,33 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let header_element = use_ref(cx, || None); + + cx.render(rsx!( + div { + h1 { + onmounted: move |cx| { + header_element.set(Some(cx.inner().clone())); + }, + "Scroll to top example" + } + + for i in 0..100 { + div { "Item {i}" } + } + + button { + onclick: move |_| { + if let Some(header) = header_element.read().as_ref() { + header.scroll_to(ScrollBehavior::Smooth); + } + }, + "Scroll to top" + } + } + )) +} diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index e94a481c7..94db28f0b 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -88,12 +88,10 @@ impl RenderedElementBacking for DesktopElement { self.id.0, focus ); - println!("script: {}", script); let fut = self .query .new_query::(&script, &self.webview) .resolve(); - println!("fut"); Box::pin(async move { match fut.await { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 8e92d90aa..701c0e0bb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -265,7 +265,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - if let Ok(result) = dbg!(serde_json::from_value::(params)) { + if let Ok(result) = serde_json::from_value::(params) { let view = webviews.get(&event.1).unwrap(); let query = view .dom From c4027c2618228b14dc00a1bbf8c9f690c11c24a3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Mar 2023 07:23:32 -0500 Subject: [PATCH 09/87] fix clippy --- examples/controll_focus.rs | 2 +- examples/read_size.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs index 0f14a940b..4f4c2ddcb 100644 --- a/examples/controll_focus.rs +++ b/examples/controll_focus.rs @@ -7,7 +7,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let elements: &UseRef>> = use_ref(cx, Vec::new); let running = use_state(cx, || true); use_future!(cx, |(elements, running)| async move { diff --git a/examples/read_size.rs b/examples/read_size.rs index be996e2dd..806f71f80 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::rc::Rc; use dioxus::{html::geometry::euclid::Rect, prelude::*}; @@ -27,7 +28,7 @@ fn main() { fn app(cx: Scope) -> Element { let div_element: &UseRef>> = use_ref(cx, || None); - let dimentions = use_ref(cx, || Rect::zero()); + let dimentions = use_ref(cx, Rect::zero); cx.render(rsx!( div { @@ -44,8 +45,10 @@ fn app(cx: Scope) -> Element { onclick: move |_| { to_owned![div_element, dimentions]; async move { - if let Some(div) = div_element.read().as_ref() { - if let Ok(rect)=div.get_client_rect().await{ + let read = div_element.read(); + let client_rect = read.as_ref().map(|el| el.get_client_rect()); + if let Some(client_rect) = client_rect { + if let Ok(rect) = client_rect.await { dimentions.set(rect); } } From 0029fed24a4d40716deaff767a0e17cd7c6baac0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Mar 2023 11:29:56 -0500 Subject: [PATCH 10/87] create complete tailwind example project with steps to setup tailwind --- Cargo.toml | 2 + examples/tailwind/Cargo.toml | 21 + examples/tailwind/Dioxus.toml | 46 + examples/tailwind/README.md | 136 +++ examples/tailwind/input.css | 3 + examples/tailwind/public/tailwind.css | 833 ++++++++++++++++++ .../{tailwind.rs => tailwind/src/main.rs} | 16 +- examples/tailwind/tailwind.config.js | 9 + 8 files changed, 1055 insertions(+), 11 deletions(-) create mode 100644 examples/tailwind/Cargo.toml create mode 100644 examples/tailwind/Dioxus.toml create mode 100644 examples/tailwind/README.md create mode 100644 examples/tailwind/input.css create mode 100644 examples/tailwind/public/tailwind.css rename examples/{tailwind.rs => tailwind/src/main.rs} (90%) create mode 100644 examples/tailwind/tailwind.config.js diff --git a/Cargo.toml b/Cargo.toml index e32940e18..7b3fc0944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "packages/signals", "packages/hot-reload", "docs/guide", + # Full project examples + "examples/tailwind", ] # This is a "virtual package" diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml new file mode 100644 index 000000000..5c2fbe666 --- /dev/null +++ b/examples/tailwind/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dioxus-tailwind" +version = "0.0.0" +authors = [] +edition = "2021" +description = "A tailwindcss example using Dioxus" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +rust-version = "1.60.0" +publish = false + +[dependencies] +dioxus = { path = "../../packages/dioxus" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dioxus-desktop = { path = "../../packages/desktop" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +dioxus-web = { path = "../../packages/web" } \ No newline at end of file diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml new file mode 100644 index 000000000..67a63e686 --- /dev/null +++ b/examples/tailwind/Dioxus.toml @@ -0,0 +1,46 @@ +[application] + +# App (Project) Name +name = "Tailwind CSS + Dioxus" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = ["tailwind.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# serve: [dev-server] only + +# CSS style file +style = [] + +# Javascript code file +script = [] diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md new file mode 100644 index 000000000..0498ecbd3 --- /dev/null +++ b/examples/tailwind/README.md @@ -0,0 +1,136 @@ +Example: Basic Tailwind usage + +This example shows how an app might be styled with TailwindCSS. + +# Setup + +1. Install the Dioxus CLI: + +```bash +cargo install --git https://github.com/DioxusLabs/cli +``` + +2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +3. Install the tailwind css cli: https://tailwindcss.com/docs/installation +4. Initialize the tailwind css project: + +```bash +npx tailwindcss init +``` + +This should create a `tailwind.config.js` file in the root of the project. + +5. Edit the `tailwind.config.js` file to include rust files: + +```json +module.exports = { + mode: "all", + content: [ + // include all rust, html and css files in the src directory + "./src/**/*.{rs,html,css}", + // include all html files in the output (dist) directory + "./dist/**/*.html", + ], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +6. Create a `input.css` file with the following content: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +7. Create a `Dioxus.toml` file with the following content that links to the `tailwind.css` file: + +```toml +[application] + +# App (Project) Name +name = "Tailwind CSS + Dioxus" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = ["tailwind.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# serve: [dev-server] only + +# CSS style file +style = [] + +# Javascript code file +script = [] +``` + +## Bonus Steps + +8. Install the tailwind css vs code extension +9. Go to the settings for the extension and find the experimental regex support section. Edit the setting.json file to look like this: + +```json +"tailwindCSS.experimental.classRegex": ["class: \"(.*)\""], +"tailwindCSS.includeLanguages": { + "rust": "html" +}, +``` + +# Development + +1. Run the following command in the root of the project to start the tailwind css compiler: + +```bash +npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch +``` + +## Web + +- Run the following command in the root of the project to start the dioxus dev server: + +```bash +dioxus serve --hot-reload +``` + +- Open the browser to http://localhost:8080 + +## Desktop + +- Launch the dioxus desktop app + +```bash +cargo run +``` diff --git a/examples/tailwind/input.css b/examples/tailwind/input.css new file mode 100644 index 000000000..bd6213e1d --- /dev/null +++ b/examples/tailwind/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/tailwind/public/tailwind.css b/examples/tailwind/public/tailwind.css new file mode 100644 index 000000000..65b95316e --- /dev/null +++ b/examples/tailwind/public/tailwind.css @@ -0,0 +1,833 @@ +/* +! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mr-5 { + margin-right: 1.25rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.hidden { + display: none; +} + +.h-10 { + height: 2.5rem; +} + +.h-4 { + height: 1rem; +} + +.w-10 { + width: 2.5rem; +} + +.w-4 { + width: 1rem; +} + +.w-5\/6 { + width: 83.333333%; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.border-0 { + border-width: 0px; +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.bg-indigo-500 { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity)); +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.object-center { + -o-object-position: center; + object-position: center; +} + +.p-2 { + padding: 0.5rem; +} + +.p-5 { + padding: 1.25rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-24 { + padding-top: 6rem; + padding-bottom: 6rem; +} + +.text-center { + text-align: center; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-medium { + font-weight: 500; +} + +.leading-relaxed { + line-height: 1.625; +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } +} + +@media (min-width: 768px) { + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:ml-auto { + margin-left: auto; + } + + .md\:mt-0 { + margin-top: 0px; + } + + .md\:w-1\/2 { + width: 50%; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:items-start { + align-items: flex-start; + } + + .md\:pr-16 { + padding-right: 4rem; + } + + .md\:text-left { + text-align: left; + } +} + +@media (min-width: 1024px) { + .lg\:inline-block { + display: inline-block; + } + + .lg\:w-full { + width: 100%; + } + + .lg\:max-w-lg { + max-width: 32rem; + } + + .lg\:flex-grow { + flex-grow: 1; + } + + .lg\:pr-24 { + padding-right: 6rem; + } +} \ No newline at end of file diff --git a/examples/tailwind.rs b/examples/tailwind/src/main.rs similarity index 90% rename from examples/tailwind.rs rename to examples/tailwind/src/main.rs index af6bc4c4f..2dbb183c9 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind/src/main.rs @@ -1,22 +1,16 @@ #![allow(non_snake_case)] -//! Example: Basic Tailwind usage -//! -//! This example shows how an app might be styled with TailwindCSS. -//! -//! To minify your tailwind bundle, currently you need to use npm. Follow these instructions: -//! -//! https://dev.to/arctic_hen7/how-to-set-up-tailwind-css-with-yew-and-trunk-il9 - use dioxus::prelude::*; -use dioxus_desktop::Config; fn main() { + #[cfg(not(target_arch = "wasm32"))] dioxus_desktop::launch_cfg( app, - Config::new() - .with_custom_head("".to_string()), + dioxus_desktop::Config::new() + .with_custom_head(r#""#.to_string()), ); + #[cfg(target_arch = "wasm32")] + dioxus_web::launch(app); } pub fn app(cx: Scope) -> Element { diff --git a/examples/tailwind/tailwind.config.js b/examples/tailwind/tailwind.config.js new file mode 100644 index 000000000..2a69d5803 --- /dev/null +++ b/examples/tailwind/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: "all", + content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], + theme: { + extend: {}, + }, + plugins: [], +}; From 7f6f6fb8c87d050d30e39d0bc2cf248d9996226e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 28 Mar 2023 13:35:17 -0500 Subject: [PATCH 11/87] create server package --- Cargo.toml | 1 + packages/server/Cargo.toml | 21 +++++ packages/server/server_macro/Cargo.toml | 8 ++ packages/server/server_macro/src/lib.rs | 67 +++++++++++++++ packages/server/src/adapters/axum_adapter.rs | 23 +++++ packages/server/src/adapters/salvo_adapter.rs | 25 ++++++ packages/server/src/adapters/warp_adapter.rs | 28 ++++++ packages/server/src/lib.rs | 85 +++++++++++++++++++ 8 files changed, 258 insertions(+) create mode 100644 packages/server/Cargo.toml create mode 100644 packages/server/server_macro/Cargo.toml create mode 100644 packages/server/server_macro/src/lib.rs create mode 100644 packages/server/src/adapters/axum_adapter.rs create mode 100644 packages/server/src/adapters/salvo_adapter.rs create mode 100644 packages/server/src/adapters/warp_adapter.rs create mode 100644 packages/server/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e32940e18..3b36bb5e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "packages/rsx-rosetta", "packages/signals", "packages/hot-reload", + "packages/server", "docs/guide", ] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml new file mode 100644 index 000000000..13704574b --- /dev/null +++ b/packages/server/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +server_fn = { version = "0.2.4", features = ["stable"] } + +# warp +warp = { version = "0.3.3", optional = true } + +# axum +axum = { version = "0.6.1", optional = true, features = ["ws"] } + +# salvo +salvo = { version = "0.37.7", optional = true, features = ["ws"] } +serde = "1.0.159" + +dioxus = { path = "../dioxus", version = "^0.3.0" } \ No newline at end of file diff --git a/packages/server/server_macro/Cargo.toml b/packages/server/server_macro/Cargo.toml new file mode 100644 index 000000000..6e8c69b46 --- /dev/null +++ b/packages/server/server_macro/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "server_macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/packages/server/server_macro/src/lib.rs b/packages/server/server_macro/src/lib.rs new file mode 100644 index 000000000..439a9e7b2 --- /dev/null +++ b/packages/server/server_macro/src/lib.rs @@ -0,0 +1,67 @@ +/// Declares that a function is a [server function](leptos_server). This means that +/// its body will only run on the server, i.e., when the `ssr` feature is enabled. +/// +/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features +/// are enabled), it will instead make a network request to the server. +/// +/// You can specify one, two, or three arguments to the server function: +/// 1. **Required**: A type name that will be used to identify and register the server function +/// (e.g., `MyServerFn`). +/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered +/// (e.g., `"/api"`). Defaults to `"/"`. +/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for +/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string). +/// Defaults to `"Url"`. If you want to use this server function to power a `

` that will +/// work without WebAssembly, the encoding must be `"Url"`. +/// +/// The server function itself can take any number of arguments, each of which should be serializable +/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope), +/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other +/// server-side context into the server function. +/// +/// ```ignore +/// # use leptos::*; use serde::{Serialize, Deserialize}; +/// # #[derive(Serialize, Deserialize)] +/// # pub struct Post { } +/// #[server(ReadPosts, "/api")] +/// pub async fn read_posts(how_many: u8, query: String) -> Result, ServerFnError> { +/// // do some work on the server to access the database +/// todo!() +/// } +/// ``` +/// +/// Note the following: +/// - You must **register** the server function by calling `T::register()` somewhere in your main function. +/// - **Server functions must be `async`.** Even if the work being done inside the function body +/// can run synchronously on the server, from the client’s perspective it involves an asynchronous +/// function call. +/// - **Server functions must return `Result`.** Even if the work being done +/// inside the function body can’t fail, the processes of serialization/deserialization and the +/// network call are fallible. +/// - **Return types must be [Serializable](leptos_reactive::Serializable).** +/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) +/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).** +/// They are serialized as an `application/x-www-form-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. +#[proc_macro_attribute] +pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { + let context = ServerContext { + ty: syn::parse_quote!(Scope), + path: syn::parse_quote!(::leptos::Scope), + }; + match server_macro_impl( + args.into(), + s.into(), + Some(context), + Some(syn::parse_quote!(::leptos::server_fn)), + ) { + Err(e) => e.to_compile_error().into(), + Ok(s) => s.to_token_stream().into(), + } +} diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs new file mode 100644 index 000000000..646cde38e --- /dev/null +++ b/packages/server/src/adapters/axum_adapter.rs @@ -0,0 +1,23 @@ +use crate::{LiveViewError, LiveViewSocket}; +use axum::extract::ws::{Message, WebSocket}; +use futures_util::{SinkExt, StreamExt}; + +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::Text(message)) +} diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs new file mode 100644 index 000000000..2c8912a57 --- /dev/null +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -0,0 +1,25 @@ +use futures_util::{SinkExt, StreamExt}; +use salvo::ws::{Message, WebSocket}; + +use crate::{LiveViewError, LiveViewSocket}; + +/// Convert a salvo websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result { + let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; + + let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; + + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) +} diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs new file mode 100644 index 000000000..e5c821ce3 --- /dev/null +++ b/packages/server/src/adapters/warp_adapter.rs @@ -0,0 +1,28 @@ +use crate::{LiveViewError, LiveViewSocket}; +use futures_util::{SinkExt, StreamExt}; +use warp::ws::{Message, WebSocket}; + +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) +} + +fn transform_rx(message: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = message + .map_err(|_| LiveViewError::SendingFailed)? + .into_bytes(); + + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; + + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) +} diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs new file mode 100644 index 000000000..49b635fc2 --- /dev/null +++ b/packages/server/src/lib.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; +use serde::{de::DeserializeOwned, Deserializer, Serialize, Serializer}; + +// We use deref specialization to make it possible to pass either a value that implements +pub trait SerializeToRemoteWrapper { + fn serialize_to_remote(&self, serializer: S) -> Result; +} + +impl SerializeToRemoteWrapper for &T { + fn serialize_to_remote( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> { + self.serialize(serializer) + } +} + +impl SerializeToRemoteWrapper for &mut &S { + fn serialize_to_remote( + &self, + serializer: S2, + ) -> Result<::Ok, ::Error> { + (**self).serialize_to_remote(serializer) + } +} + +pub trait SerializeToRemote { + fn serialize_to_remote(&self, serializer: S) -> Result; +} + +impl SerializeToRemote for UseState { + fn serialize_to_remote( + &self, + serializer: S2, + ) -> Result<::Ok, ::Error> { + self.current().serialize(serializer) + } +} + +// We use deref specialization to make it possible to pass either a value that implements +pub trait DeserializeOnRemoteWrapper { + type Output; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result; +} + +impl DeserializeOnRemoteWrapper for &T { + type Output = T; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result { + T::deserialize(deserializer) + } +} + +impl DeserializeOnRemoteWrapper for &mut &D { + type Output = D::Output; + + fn deserialize_on_remote<'a, D2: Deserializer<'a>>( + deserializer: D2, + ) -> Result { + D::deserialize_on_remote(deserializer) + } +} + +pub trait DeserializeOnRemote { + type Output; + + fn deserialize_on_remote<'a, D: Deserializer<'a>>( + deserializer: D, + ) -> Result; +} + +impl DeserializeOnRemote for UseState { + type Output = D; + + fn deserialize_on_remote<'a, D2: Deserializer<'a>>( + deserializer: D2, + ) -> Result { + D::deserialize(deserializer) + } +} From 939e75541e9bfbe67f0188df556e256b3032e343 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 29 Mar 2023 20:20:26 -0500 Subject: [PATCH 12/87] initial axum implementation --- packages/server/Cargo.toml | 15 +- packages/server/src/adapters/axum_adapter.rs | 116 +++++++++++-- packages/server/src/adapters/mod.rs | 2 + packages/server/src/lib.rs | 173 ++++++++++--------- 4 files changed, 211 insertions(+), 95 deletions(-) create mode 100644 packages/server/src/adapters/mod.rs diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 13704574b..4e3028aaa 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -18,4 +18,17 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] } salvo = { version = "0.37.7", optional = true, features = ["ws"] } serde = "1.0.159" -dioxus = { path = "../dioxus", version = "^0.3.0" } \ No newline at end of file +dioxus = { path = "../dioxus", version = "^0.3.0" } + +log = "0.4.17" +once_cell = "1.17.1" +thiserror = "1.0.40" +hyper = "0.14.25" +tokio = { version = "1.27.0", features = ["full"] } + +[features] +default = ["axum", "ssr"] +warp = ["dep:warp"] +axum = ["dep:axum"] +salvo = ["dep:salvo"] +ssr = ["server_fn/ssr"] diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 646cde38e..3fb0a1d4e 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -1,23 +1,105 @@ -use crate::{LiveViewError, LiveViewSocket}; -use axum::extract::ws::{Message, WebSocket}; -use futures_util::{SinkExt, StreamExt}; +use std::{error::Error, sync::Arc}; -/// Convert a warp websocket into a LiveViewSocket -/// -/// This is required to launch a LiveView app using the warp web framework -pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { - ws.map(transform_rx) - .with(transform_tx) - .sink_map_err(|_| LiveViewError::SendingFailed) +use axum::{ + body::{self, Body, BoxBody, Full}, + http::{HeaderMap, Request, Response, StatusCode}, + response::IntoResponse, + routing::post, + Router, +}; +use server_fn::{Payload, ServerFunctionRegistry}; +use tokio::task::spawn_blocking; + +use crate::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}; + +trait DioxusRouterExt { + fn register_server_fns(self) -> Self; } -fn transform_rx(message: Result) -> Result { - message - .map_err(|_| LiveViewError::SendingFailed)? - .into_text() - .map_err(|_| LiveViewError::SendingFailed) +impl DioxusRouterExt for Router { + fn register_server_fns(self) -> Self { + let mut router = self; + for server_fn_path in DioxusServerFnRegistry::paths_registered() { + let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + router = router.route( + server_fn_path, + post(move |headers: HeaderMap, body: Request| async move { + server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await + // todo!() + }), + ); + } + router + } } -async fn transform_tx(message: String) -> Result { - Ok(Message::Text(message)) +async fn server_fn_handler( + server_context: DioxusServerContext, + function: Arc, + headers: HeaderMap, + req: Request, +) -> impl IntoResponse { + let (_, body) = req.into_parts(); + let body = hyper::body::to_bytes(body).await; + let Ok(body)=body else { + return report_err(body.err().unwrap()); + }; + + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + spawn_blocking({ + move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async { + let resp = match function(server_context, &body).await { + Ok(serialized) => { + // if this is Accept: application/json then send a serialized JSON response + let accept_header = + headers.get("Accept").and_then(|value| value.to_str().ok()); + let mut res = Response::builder(); + if accept_header == Some("application/json") + || accept_header + == Some( + "application/\ + x-www-form-urlencoded", + ) + || accept_header == Some("application/cbor") + { + res = res.status(StatusCode::OK); + } + + let resp = match serialized { + Payload::Binary(data) => res + .header("Content-Type", "application/cbor") + .body(body::boxed(Full::from(data))), + Payload::Url(data) => res + .header( + "Content-Type", + "application/\ + x-www-form-urlencoded", + ) + .body(body::boxed(data)), + Payload::Json(data) => res + .header("Content-Type", "application/json") + .body(body::boxed(data)), + }; + + resp.unwrap() + } + Err(e) => report_err(e), + }; + + resp_tx.send(resp).unwrap(); + }) + } + }); + resp_rx.await.unwrap() +} + +fn report_err(e: E) -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(body::boxed(format!("Error: {}", e))) + .unwrap() } diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs new file mode 100644 index 000000000..ce2670dde --- /dev/null +++ b/packages/server/src/adapters/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "axum")] +mod axum_adapter; diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 49b635fc2..c0ec07e06 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,85 +1,104 @@ -use dioxus::prelude::*; -use serde::{de::DeserializeOwned, Deserializer, Serialize, Serializer}; +mod adapters; -// We use deref specialization to make it possible to pass either a value that implements -pub trait SerializeToRemoteWrapper { - fn serialize_to_remote(&self, serializer: S) -> Result; -} +// #[server(ReadPosts, "api")] +// async fn testing(rx: i32) -> Result { +// Ok(0) +// } -impl SerializeToRemoteWrapper for &T { - fn serialize_to_remote( - &self, - serializer: S, - ) -> Result<::Ok, ::Error> { - self.serialize(serializer) +pub struct DioxusServerContext {} + +#[cfg(any(feature = "ssr", doc))] +type ServerFnTraitObj = server_fn::ServerFnTraitObj; + +#[cfg(any(feature = "ssr", doc))] +static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< + std::sync::Arc< + std::sync::RwLock< + std::collections::HashMap<&'static str, std::sync::Arc>, + >, + >, +> = once_cell::sync::Lazy::new(Default::default); + +#[cfg(any(feature = "ssr", doc))] +/// The registry of all Dioxus server functions. +pub struct DioxusServerFnRegistry; + +#[cfg(any(feature = "ssr"))] +impl server_fn::ServerFunctionRegistry for DioxusServerFnRegistry { + type Error = ServerRegistrationFnError; + + fn register( + url: &'static str, + server_function: std::sync::Arc, + ) -> Result<(), Self::Error> { + // store it in the hashmap + let mut write = REGISTERED_SERVER_FUNCTIONS + .write() + .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; + let prev = write.insert(url, server_function); + + // if there was already a server function with this key, + // return Err + match prev { + Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!( + "There was already a server function registered at {:?}. \ + This can happen if you use the same server function name \ + in two different modules + on `stable` or in `release` mode.", + url + ))), + None => Ok(()), + } + } + + /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. + fn get(url: &str) -> Option> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .and_then(|fns| fns.get(url).cloned()) + } + + /// Returns a list of all registered server functions. + fn paths_registered() -> Vec<&'static str> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .map(|fns| fns.keys().cloned().collect()) + .unwrap_or_default() } } -impl SerializeToRemoteWrapper for &mut &S { - fn serialize_to_remote( - &self, - serializer: S2, - ) -> Result<::Ok, ::Error> { - (**self).serialize_to_remote(serializer) +#[cfg(any(feature = "ssr", doc))] +/// Errors that can occur when registering a server function. +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ServerRegistrationFnError { + /// The server function is already registered. + #[error("The server function {0} is already registered")] + AlreadyRegistered(String), + /// The server function registry is poisoned. + #[error("The server function registry is poisoned: {0}")] + Poisoned(String), +} + +/// 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. +/// +/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, +/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) +/// +/// Server functions are created using the `server` macro. +/// +/// The function should be registered by calling `ServerFn::register()`. The set of server functions +/// can be queried on the server for routing purposes by calling [server_fn_by_path]. +/// +/// Technically, the trait is implemented on a type that describes the server function's arguments. +pub trait ServerFn: server_fn::ServerFn { + /// Registers the server function, allowing the server to query it by URL. + #[cfg(any(feature = "ssr", doc))] + fn register() -> Result<(), server_fn::ServerFnError> { + Self::register_in::() } } -pub trait SerializeToRemote { - fn serialize_to_remote(&self, serializer: S) -> Result; -} - -impl SerializeToRemote for UseState { - fn serialize_to_remote( - &self, - serializer: S2, - ) -> Result<::Ok, ::Error> { - self.current().serialize(serializer) - } -} - -// We use deref specialization to make it possible to pass either a value that implements -pub trait DeserializeOnRemoteWrapper { - type Output; - - fn deserialize_on_remote<'a, D: Deserializer<'a>>( - deserializer: D, - ) -> Result; -} - -impl DeserializeOnRemoteWrapper for &T { - type Output = T; - - fn deserialize_on_remote<'a, D: Deserializer<'a>>( - deserializer: D, - ) -> Result { - T::deserialize(deserializer) - } -} - -impl DeserializeOnRemoteWrapper for &mut &D { - type Output = D::Output; - - fn deserialize_on_remote<'a, D2: Deserializer<'a>>( - deserializer: D2, - ) -> Result { - D::deserialize_on_remote(deserializer) - } -} - -pub trait DeserializeOnRemote { - type Output; - - fn deserialize_on_remote<'a, D: Deserializer<'a>>( - deserializer: D, - ) -> Result; -} - -impl DeserializeOnRemote for UseState { - type Output = D; - - fn deserialize_on_remote<'a, D2: Deserializer<'a>>( - deserializer: D2, - ) -> Result { - D::deserialize(deserializer) - } -} +impl ServerFn for T where T: server_fn::ServerFn {} From fdc8ebd1b196fddf5abd2cbc50cbd98c9fc0f912 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 10:34:13 -0500 Subject: [PATCH 13/87] create fullstack hello world example --- packages/server/.gitignore | 1 + packages/server/Cargo.toml | 23 ++- .../server/examples/hello-world/.gitignore | 2 + .../server/examples/hello-world/Cargo.toml | 20 +++ .../server/examples/hello-world/src/main.rs | 70 +++++++++ packages/server/server_macro/Cargo.toml | 6 + packages/server/server_macro/src/lib.rs | 10 +- packages/server/src/adapters/axum_adapter.rs | 49 +++++- packages/server/src/adapters/mod.rs | 2 +- packages/server/src/lib.rs | 148 ++++++------------ packages/server/src/server_fn.rs | 98 ++++++++++++ 11 files changed, 312 insertions(+), 117 deletions(-) create mode 100644 packages/server/.gitignore create mode 100644 packages/server/examples/hello-world/.gitignore create mode 100644 packages/server/examples/hello-world/Cargo.toml create mode 100644 packages/server/examples/hello-world/src/main.rs create mode 100644 packages/server/src/server_fn.rs diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 000000000..1de565933 --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 4e3028aaa..af7e03859 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "server" +name = "dioxus-server" version = "0.1.0" edition = "2021" @@ -7,28 +7,37 @@ edition = "2021" [dependencies] server_fn = { version = "0.2.4", features = ["stable"] } +server_macro = { path = "server_macro" } # warp warp = { version = "0.3.3", optional = true } # axum axum = { version = "0.6.1", optional = true, features = ["ws"] } +tower-http = { version = "0.4.0", optional = true, features = ["fs"] } +hyper = { version = "0.14.25", optional = true } # salvo salvo = { version = "0.37.7", optional = true, features = ["ws"] } serde = "1.0.159" -dioxus = { path = "../dioxus", version = "^0.3.0" } +dioxus-core = { path = "../core", version = "^0.3.0" } +dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true } log = "0.4.17" once_cell = "1.17.1" thiserror = "1.0.40" -hyper = "0.14.25" -tokio = { version = "1.27.0", features = ["full"] } +tokio = { version = "1.27.0", features = ["full"], optional = true } [features] -default = ["axum", "ssr"] +default = [] warp = ["dep:warp"] -axum = ["dep:axum"] +axum = ["dep:axum", "tower-http", "hyper"] salvo = ["dep:salvo"] -ssr = ["server_fn/ssr"] +ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] + +[workspace] +members = [ + "server_macro", + "examples/hello-world", +] \ No newline at end of file diff --git a/packages/server/examples/hello-world/.gitignore b/packages/server/examples/hello-world/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/hello-world/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/hello-world/Cargo.toml b/packages/server/examples/hello-world/Cargo.toml new file mode 100644 index 000000000..846c03ea4 --- /dev/null +++ b/packages/server/examples/hello-world/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-server = { path = "../../" } +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tracing-subscriber = "0.3.16" +tracing = "0.1.37" + +[features] +ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum"] +web = ["dioxus-web"] diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/hello-world/src/main.rs new file mode 100644 index 000000000..9b9084963 --- /dev/null +++ b/packages/server/examples/hello-world/src/main.rs @@ -0,0 +1,70 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .serve_dioxus_application( + "Hello, world!", + "hello-world", + None, + None, + "", + app, + ) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/server_macro/Cargo.toml b/packages/server/server_macro/Cargo.toml index 6e8c69b46..cb285f517 100644 --- a/packages/server/server_macro/Cargo.toml +++ b/packages/server/server_macro/Cargo.toml @@ -6,3 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +quote = "1.0.26" +server_fn_macro = { version = "0.2.4", features = ["stable"] } +syn = { version = "1", features = ["full"] } + +[lib] +proc-macro = true diff --git a/packages/server/server_macro/src/lib.rs b/packages/server/server_macro/src/lib.rs index 439a9e7b2..f2a2e1665 100644 --- a/packages/server/server_macro/src/lib.rs +++ b/packages/server/server_macro/src/lib.rs @@ -1,3 +1,7 @@ +use proc_macro::TokenStream; +use quote::ToTokens; +use server_fn_macro::*; + /// Declares that a function is a [server function](leptos_server). This means that /// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// @@ -52,14 +56,14 @@ #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { let context = ServerContext { - ty: syn::parse_quote!(Scope), - path: syn::parse_quote!(::leptos::Scope), + ty: syn::parse_quote!(DioxusServerContext), + path: syn::parse_quote!(::dioxus_server::prelude::DioxusServerContext), }; match server_macro_impl( args.into(), s.into(), Some(context), - Some(syn::parse_quote!(::leptos::server_fn)), + Some(syn::parse_quote!(::dioxus_server::prelude::server_fn)), ) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 3fb0a1d4e..70513bdb3 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -4,33 +4,68 @@ use axum::{ body::{self, Body, BoxBody, Full}, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, - routing::post, + routing::{get, post}, Router, }; +use dioxus_core::Component; use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; -use crate::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}; +use crate::{ + dioxus_ssr_html, + server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, +}; -trait DioxusRouterExt { - fn register_server_fns(self) -> Self; +pub trait DioxusRouterExt { + fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( + self, + title: &'static str, + application_name: &'static str, + base_path: Option<&'static str>, + head: Option<&'static str>, + server_fn_route: &'static str, + app: Component, + ) -> Self; } impl DioxusRouterExt for Router { - fn register_server_fns(self) -> Self { + fn register_server_fns(self, server_fn_route: &'static str) -> Self { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + let full_route = format!("{server_fn_route}/{server_fn_path}"); router = router.route( - server_fn_path, + &full_route, post(move |headers: HeaderMap, body: Request| async move { server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await - // todo!() }), ); } router } + + fn serve_dioxus_application( + self, + title: &'static str, + application_name: &'static str, + base_path: Option<&'static str>, + head: Option<&'static str>, + server_fn_route: &'static str, + app: Component, + ) -> Self { + use tower_http::services::ServeDir; + + // Serve the dist folder and the index.html file + let serve_dir = ServeDir::new("dist"); + + self.register_server_fns(server_fn_route) + .nest_service("/", serve_dir) + .fallback_service(get(move || { + let rendered = dioxus_ssr_html(title, application_name, base_path, head, app); + async move { Full::from(rendered) } + })) + } } async fn server_fn_handler( diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index ce2670dde..c30365989 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -1,2 +1,2 @@ #[cfg(feature = "axum")] -mod axum_adapter; +pub mod axum_adapter; diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index c0ec07e06..c78f3471b 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,104 +1,54 @@ +#[allow(unused)] +use dioxus_core::prelude::*; + mod adapters; +mod server_fn; -// #[server(ReadPosts, "api")] -// async fn testing(rx: i32) -> Result { -// Ok(0) -// } - -pub struct DioxusServerContext {} - -#[cfg(any(feature = "ssr", doc))] -type ServerFnTraitObj = server_fn::ServerFnTraitObj; - -#[cfg(any(feature = "ssr", doc))] -static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< - std::sync::Arc< - std::sync::RwLock< - std::collections::HashMap<&'static str, std::sync::Arc>, - >, - >, -> = once_cell::sync::Lazy::new(Default::default); - -#[cfg(any(feature = "ssr", doc))] -/// The registry of all Dioxus server functions. -pub struct DioxusServerFnRegistry; - -#[cfg(any(feature = "ssr"))] -impl server_fn::ServerFunctionRegistry for DioxusServerFnRegistry { - type Error = ServerRegistrationFnError; - - fn register( - url: &'static str, - server_function: std::sync::Arc, - ) -> Result<(), Self::Error> { - // store it in the hashmap - let mut write = REGISTERED_SERVER_FUNCTIONS - .write() - .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; - let prev = write.insert(url, server_function); - - // if there was already a server function with this key, - // return Err - match prev { - Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!( - "There was already a server function registered at {:?}. \ - This can happen if you use the same server function name \ - in two different modules - on `stable` or in `release` mode.", - url - ))), - None => Ok(()), - } - } - - /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get(url: &str) -> Option> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .and_then(|fns| fns.get(url).cloned()) - } - - /// Returns a list of all registered server functions. - fn paths_registered() -> Vec<&'static str> { - REGISTERED_SERVER_FUNCTIONS - .read() - .ok() - .map(|fns| fns.keys().cloned().collect()) - .unwrap_or_default() - } +pub mod prelude { + #[cfg(feature = "axum")] + pub use crate::adapters::axum_adapter::*; + pub use crate::server_fn::{DioxusServerContext, ServerFn}; + pub use server_fn::{self, ServerFn as _, ServerFnError}; + pub use server_macro::*; } -#[cfg(any(feature = "ssr", doc))] -/// Errors that can occur when registering a server function. -#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ServerRegistrationFnError { - /// The server function is already registered. - #[error("The server function {0} is already registered")] - AlreadyRegistered(String), - /// The server function registry is poisoned. - #[error("The server function registry is poisoned: {0}")] - Poisoned(String), +#[cfg(feature = "ssr")] +fn dioxus_ssr_html( + title: &str, + application_name: &str, + base_path: Option<&str>, + head: Option<&str>, + app: Component, +) -> String { + let mut vdom = VirtualDom::new(app); + let _ = vdom.rebuild(); + let renderered = dioxus_ssr::pre_render(&vdom); + let base_path = base_path.unwrap_or("."); + let head = head.unwrap_or_default(); + format!( + r#" + + + + {title} + + + + {head} + + +
+ {renderered} +
+ + +"# + ) } - -/// 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. -/// -/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, -/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) -/// -/// Server functions are created using the `server` macro. -/// -/// The function should be registered by calling `ServerFn::register()`. The set of server functions -/// can be queried on the server for routing purposes by calling [server_fn_by_path]. -/// -/// Technically, the trait is implemented on a type that describes the server function's arguments. -pub trait ServerFn: server_fn::ServerFn { - /// Registers the server function, allowing the server to query it by URL. - #[cfg(any(feature = "ssr", doc))] - fn register() -> Result<(), server_fn::ServerFnError> { - Self::register_in::() - } -} - -impl ServerFn for T where T: server_fn::ServerFn {} diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs new file mode 100644 index 000000000..28c7cd9b6 --- /dev/null +++ b/packages/server/src/server_fn.rs @@ -0,0 +1,98 @@ +pub struct DioxusServerContext {} + +#[cfg(any(feature = "ssr", doc))] +pub type ServerFnTraitObj = server_fn::ServerFnTraitObj; + +#[cfg(any(feature = "ssr", doc))] +#[allow(clippy::type_complexity)] +static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< + std::sync::Arc< + std::sync::RwLock< + std::collections::HashMap<&'static str, std::sync::Arc>, + >, + >, +> = once_cell::sync::Lazy::new(Default::default); + +#[cfg(any(feature = "ssr", doc))] +/// The registry of all Dioxus server functions. +pub struct DioxusServerFnRegistry; + +#[cfg(any(feature = "ssr"))] +impl server_fn::ServerFunctionRegistry for DioxusServerFnRegistry { + type Error = ServerRegistrationFnError; + + fn register( + url: &'static str, + server_function: std::sync::Arc, + ) -> Result<(), Self::Error> { + // store it in the hashmap + let mut write = REGISTERED_SERVER_FUNCTIONS + .write() + .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; + let prev = write.insert(url, server_function); + + // if there was already a server function with this key, + // return Err + match prev { + Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!( + "There was already a server function registered at {:?}. \ + This can happen if you use the same server function name \ + in two different modules + on `stable` or in `release` mode.", + url + ))), + None => Ok(()), + } + } + + /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. + fn get(url: &str) -> Option> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .and_then(|fns| fns.get(url).cloned()) + } + + /// Returns a list of all registered server functions. + fn paths_registered() -> Vec<&'static str> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .map(|fns| fns.keys().cloned().collect()) + .unwrap_or_default() + } +} + +#[cfg(any(feature = "ssr", doc))] +/// Errors that can occur when registering a server function. +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ServerRegistrationFnError { + /// The server function is already registered. + #[error("The server function {0} is already registered")] + AlreadyRegistered(String), + /// The server function registry is poisoned. + #[error("The server function registry is poisoned: {0}")] + Poisoned(String), +} + +/// 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. +/// +/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, +/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) +/// +/// Server functions are created using the `server` macro. +/// +/// The function should be registered by calling `ServerFn::register()`. The set of server functions +/// can be queried on the server for routing purposes by calling [server_fn_by_path]. +/// +/// Technically, the trait is implemented on a type that describes the server function's arguments. +pub trait ServerFn: server_fn::ServerFn { + /// Registers the server function, allowing the server to query it by URL. + #[cfg(any(feature = "ssr", doc))] + fn register() -> Result<(), server_fn::ServerFnError> { + Self::register_in::() + } +} + +impl ServerFn for T where T: server_fn::ServerFn {} From 1d395d572f2f642174378cf2a1e3e99695702726 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 10:38:32 -0500 Subject: [PATCH 14/87] fix workspace build --- Cargo.toml | 2 ++ packages/server/Cargo.toml | 8 +------- packages/server/{server_macro => server-macro}/Cargo.toml | 0 packages/server/{server_macro => server-macro}/src/lib.rs | 0 4 files changed, 3 insertions(+), 7 deletions(-) rename packages/server/{server_macro => server-macro}/Cargo.toml (100%) rename packages/server/{server_macro => server-macro}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 3b36bb5e3..8389566ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "packages/signals", "packages/hot-reload", "packages/server", + "packages/server/server-macro", + "packages/server/examples/hello-world", "docs/guide", ] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index af7e03859..57ad88da1 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] server_fn = { version = "0.2.4", features = ["stable"] } -server_macro = { path = "server_macro" } +server_macro = { path = "server-macro" } # warp warp = { version = "0.3.3", optional = true } @@ -35,9 +35,3 @@ warp = ["dep:warp"] axum = ["dep:axum", "tower-http", "hyper"] salvo = ["dep:salvo"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] - -[workspace] -members = [ - "server_macro", - "examples/hello-world", -] \ No newline at end of file diff --git a/packages/server/server_macro/Cargo.toml b/packages/server/server-macro/Cargo.toml similarity index 100% rename from packages/server/server_macro/Cargo.toml rename to packages/server/server-macro/Cargo.toml diff --git a/packages/server/server_macro/src/lib.rs b/packages/server/server-macro/src/lib.rs similarity index 100% rename from packages/server/server_macro/src/lib.rs rename to packages/server/server-macro/src/lib.rs From 1be48c4aa8cdb45f5a35014e59c4f62b8d21a906 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 11:03:07 -0500 Subject: [PATCH 15/87] create a serveconfig builder --- .../server/examples/hello-world/src/main.rs | 7 +-- packages/server/src/adapters/axum_adapter.rs | 26 ++-------- packages/server/src/lib.rs | 33 ++++++++----- packages/server/src/serve.rs | 47 +++++++++++++++++++ 4 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 packages/server/src/serve.rs diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/hello-world/src/main.rs index 9b9084963..9cafe93b5 100644 --- a/packages/server/examples/hello-world/src/main.rs +++ b/packages/server/examples/hello-world/src/main.rs @@ -17,12 +17,7 @@ fn main() { .serve( axum::Router::new() .serve_dioxus_application( - "Hello, world!", - "hello-world", - None, - None, - "", - app, + ServeConfig::new(app).head(r#"Hello World!"#), ) .into_make_service(), ) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 70513bdb3..649629f61 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -7,26 +7,18 @@ use axum::{ routing::{get, post}, Router, }; -use dioxus_core::Component; use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ dioxus_ssr_html, + serve::ServeConfig, server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, }; pub trait DioxusRouterExt { fn register_server_fns(self, server_fn_route: &'static str) -> Self; - fn serve_dioxus_application( - self, - title: &'static str, - application_name: &'static str, - base_path: Option<&'static str>, - head: Option<&'static str>, - server_fn_route: &'static str, - app: Component, - ) -> Self; + fn serve_dioxus_application(self, cfg: ServeConfig) -> Self; } impl DioxusRouterExt for Router { @@ -45,24 +37,16 @@ impl DioxusRouterExt for Router { router } - fn serve_dioxus_application( - self, - title: &'static str, - application_name: &'static str, - base_path: Option<&'static str>, - head: Option<&'static str>, - server_fn_route: &'static str, - app: Component, - ) -> Self { + fn serve_dioxus_application(self, cfg: ServeConfig) -> Self { use tower_http::services::ServeDir; // Serve the dist folder and the index.html file let serve_dir = ServeDir::new("dist"); - self.register_server_fns(server_fn_route) + self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) .nest_service("/", serve_dir) .fallback_service(get(move || { - let rendered = dioxus_ssr_html(title, application_name, base_path, head, app); + let rendered = dioxus_ssr_html(cfg); async move { Full::from(rendered) } })) } diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index c78f3471b..e14a445b5 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -2,38 +2,47 @@ use dioxus_core::prelude::*; mod adapters; +mod serve; mod server_fn; pub mod prelude { #[cfg(feature = "axum")] pub use crate::adapters::axum_adapter::*; + pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_macro::*; } #[cfg(feature = "ssr")] -fn dioxus_ssr_html( - title: &str, - application_name: &str, - base_path: Option<&str>, - head: Option<&str>, - app: Component, -) -> String { +fn dioxus_ssr_html(cfg: serve::ServeConfig) -> String { + use prelude::ServeConfig; + + let ServeConfig { + app, + application_name, + base_path, + head, + .. + } = cfg; + + let application_name = application_name.unwrap_or("dioxus"); + let mut vdom = VirtualDom::new(app); let _ = vdom.rebuild(); let renderered = dioxus_ssr::pre_render(&vdom); let base_path = base_path.unwrap_or("."); - let head = head.unwrap_or_default(); + let head = head.unwrap_or( + r#"Dioxus Application + + + "#, + ); format!( r#" - {title} - - - {head} diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs new file mode 100644 index 000000000..7dc1eaaac --- /dev/null +++ b/packages/server/src/serve.rs @@ -0,0 +1,47 @@ +use dioxus_core::Component; + +#[derive(Clone)] +pub struct ServeConfig { + pub(crate) app: Component, + pub(crate) application_name: Option<&'static str>, + pub(crate) server_fn_route: Option<&'static str>, + pub(crate) base_path: Option<&'static str>, + pub(crate) head: Option<&'static str>, +} + +impl ServeConfig { + /// Create a new ServeConfig + pub fn new(app: Component) -> Self { + Self { + app, + application_name: None, + server_fn_route: None, + base_path: None, + head: None, + } + } + + /// Set the application name matching the name in the Dioxus.toml file used to build the application + pub fn application_name(mut self, application_name: &'static str) -> Self { + self.application_name = Some(application_name); + self + } + + /// Set the base route all server functions will be served under + pub fn server_fn_route(mut self, server_fn_route: &'static str) -> Self { + self.server_fn_route = Some(server_fn_route); + self + } + + /// Set the path the WASM application will be served under + pub fn base_path(mut self, base_path: &'static str) -> Self { + self.base_path = Some(base_path); + self + } + + /// Set the head content to be included in the HTML document served + pub fn head(mut self, head: &'static str) -> Self { + self.head = Some(head); + self + } +} From 39a5fbf26861c10be16abeb403378c7136137f24 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 15:58:03 -0500 Subject: [PATCH 16/87] add build instuctions --- packages/server/examples/hello-world/src/main.rs | 7 +++++++ packages/server/src/adapters/axum_adapter.rs | 4 ++-- packages/server/src/lib.rs | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/hello-world/src/main.rs index 9cafe93b5..2fd6b1bde 100644 --- a/packages/server/examples/hello-world/src/main.rs +++ b/packages/server/examples/hello-world/src/main.rs @@ -1,3 +1,10 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 649629f61..b7cc85e1c 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -44,11 +44,11 @@ impl DioxusRouterExt for Router { let serve_dir = ServeDir::new("dist"); self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) - .nest_service("/", serve_dir) - .fallback_service(get(move || { + .route("/", get(move || { let rendered = dioxus_ssr_html(cfg); async move { Full::from(rendered) } })) + .fallback_service( serve_dir) } } diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index e14a445b5..98261d1d4 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -2,12 +2,14 @@ use dioxus_core::prelude::*; mod adapters; +#[cfg(feature = "ssr")] mod serve; mod server_fn; pub mod prelude { #[cfg(feature = "axum")] pub use crate::adapters::axum_adapter::*; + #[cfg(feature = "ssr")] pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; pub use server_fn::{self, ServerFn as _, ServerFnError}; From f618da7311df3766fa96faf03485853c6d4cd716 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 19:42:46 -0500 Subject: [PATCH 17/87] allow passing props to the rendered component --- Cargo.toml | 3 ++- packages/server/Cargo.toml | 6 +++--- packages/server/src/adapters/mod.rs | 2 ++ packages/server/src/lib.rs | 7 +++++-- packages/server/src/serve.rs | 10 ++++++---- packages/server/src/server_fn.rs | 1 + 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8389566ee..bf2b4b8c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,8 @@ members = [ "packages/hot-reload", "packages/server", "packages/server/server-macro", - "packages/server/examples/hello-world", + "packages/server/examples/axum-hello-world", + "packages/server/examples/salvo-hello-world", "docs/guide", ] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 57ad88da1..fc5f33c7f 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -13,12 +13,12 @@ server_macro = { path = "server-macro" } warp = { version = "0.3.3", optional = true } # axum -axum = { version = "0.6.1", optional = true, features = ["ws"] } +axum = { version = "0.6.1", optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs"] } hyper = { version = "0.14.25", optional = true } # salvo -salvo = { version = "0.37.7", optional = true, features = ["ws"] } +salvo = { version = "0.37.7", optional = true, features = ["serve-static"] } serde = "1.0.159" dioxus-core = { path = "../core", version = "^0.3.0" } @@ -33,5 +33,5 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } default = [] warp = ["dep:warp"] axum = ["dep:axum", "tower-http", "hyper"] -salvo = ["dep:salvo"] +salvo = ["dep:salvo", "hyper"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index c30365989..41bb4a403 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -1,2 +1,4 @@ #[cfg(feature = "axum")] pub mod axum_adapter; +#[cfg(feature = "salvo")] +pub mod salvo_adapter; diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 98261d1d4..e7559363a 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -9,6 +9,8 @@ mod server_fn; pub mod prelude { #[cfg(feature = "axum")] pub use crate::adapters::axum_adapter::*; + #[cfg(feature = "salvo")] + pub use crate::adapters::salvo_adapter::*; #[cfg(feature = "ssr")] pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; @@ -17,7 +19,7 @@ pub mod prelude { } #[cfg(feature = "ssr")] -fn dioxus_ssr_html(cfg: serve::ServeConfig) -> String { +fn dioxus_ssr_html(cfg: &serve::ServeConfig

) -> String { use prelude::ServeConfig; let ServeConfig { @@ -25,12 +27,13 @@ fn dioxus_ssr_html(cfg: serve::ServeConfig) -> String { application_name, base_path, head, + props, .. } = cfg; let application_name = application_name.unwrap_or("dioxus"); - let mut vdom = VirtualDom::new(app); + let mut vdom = VirtualDom::new_with_props(*app, props.clone()); let _ = vdom.rebuild(); let renderered = dioxus_ssr::pre_render(&vdom); let base_path = base_path.unwrap_or("."); diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs index 7dc1eaaac..9cca17a6b 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve.rs @@ -1,19 +1,21 @@ use dioxus_core::Component; #[derive(Clone)] -pub struct ServeConfig { - pub(crate) app: Component, +pub struct ServeConfig { + pub(crate) app: Component

, + pub(crate) props: P, pub(crate) application_name: Option<&'static str>, pub(crate) server_fn_route: Option<&'static str>, pub(crate) base_path: Option<&'static str>, pub(crate) head: Option<&'static str>, } -impl ServeConfig { +impl ServeConfig

{ /// Create a new ServeConfig - pub fn new(app: Component) -> Self { + pub fn new(app: Component

, props: P) -> Self { Self { app, + props, application_name: None, server_fn_route: None, base_path: None, diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 28c7cd9b6..2f000adda 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct DioxusServerContext {} #[cfg(any(feature = "ssr", doc))] From 83d513ef362f6cc17bd18d4c53bd9db7ebea3450 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 19:42:53 -0500 Subject: [PATCH 18/87] add salvo intigration --- .../.gitignore | 0 .../Cargo.toml | 2 +- .../src/main.rs | 4 +- .../examples/salvo-hello-world/.gitignore | 2 + .../examples/salvo-hello-world/Cargo.toml | 20 +++ .../examples/salvo-hello-world/src/main.rs | 68 ++++++++ packages/server/src/adapters/salvo_adapter.rs | 161 ++++++++++++++++-- 7 files changed, 238 insertions(+), 19 deletions(-) rename packages/server/examples/{hello-world => axum-hello-world}/.gitignore (100%) rename packages/server/examples/{hello-world => axum-hello-world}/Cargo.toml (95%) rename packages/server/examples/{hello-world => axum-hello-world}/src/main.rs (95%) create mode 100644 packages/server/examples/salvo-hello-world/.gitignore create mode 100644 packages/server/examples/salvo-hello-world/Cargo.toml create mode 100644 packages/server/examples/salvo-hello-world/src/main.rs diff --git a/packages/server/examples/hello-world/.gitignore b/packages/server/examples/axum-hello-world/.gitignore similarity index 100% rename from packages/server/examples/hello-world/.gitignore rename to packages/server/examples/axum-hello-world/.gitignore diff --git a/packages/server/examples/hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml similarity index 95% rename from packages/server/examples/hello-world/Cargo.toml rename to packages/server/examples/axum-hello-world/Cargo.toml index 846c03ea4..54aaf17d3 100644 --- a/packages/server/examples/hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hello-world" +name = "axum-hello-world" version = "0.1.0" edition = "2021" diff --git a/packages/server/examples/hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs similarity index 95% rename from packages/server/examples/hello-world/src/main.rs rename to packages/server/examples/axum-hello-world/src/main.rs index 2fd6b1bde..4d4ab5e78 100644 --- a/packages/server/examples/hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -1,5 +1,5 @@ //! Run with: -//! +//! //! ```sh //! dioxus build --features web //! cargo run --features ssr @@ -24,7 +24,7 @@ fn main() { .serve( axum::Router::new() .serve_dioxus_application( - ServeConfig::new(app).head(r#"Hello World!"#), + ServeConfig::new(app, ()).head(r#"Hello World!"#), ) .into_make_service(), ) diff --git a/packages/server/examples/salvo-hello-world/.gitignore b/packages/server/examples/salvo-hello-world/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/salvo-hello-world/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml new file mode 100644 index 000000000..1ebe56dfc --- /dev/null +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "salvo-hello-world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-server = { path = "../../" } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tracing-subscriber = "0.3.16" +tracing = "0.1.37" +salvo = { version = "0.37.9", optional = true } + +[features] +ssr = ["salvo", "tokio", "dioxus-server/ssr", "dioxus-server/salvo"] +web = ["dioxus-web"] diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs new file mode 100644 index 000000000..cd8b80c51 --- /dev/null +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -0,0 +1,68 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + use salvo::prelude::*; + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let router = Router::new().serve_dioxus_application( + ServeConfig::new(app, ()).head(r#"Hello World!"#), + ); + Server::new(TcpListener::bind("127.0.0.1:8080")) + .serve(router) + .await; + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 2c8912a57..20edc9364 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -1,25 +1,154 @@ -use futures_util::{SinkExt, StreamExt}; -use salvo::ws::{Message, WebSocket}; +use std::{error::Error, sync::Arc}; -use crate::{LiveViewError, LiveViewSocket}; +use hyper::{http::HeaderValue, StatusCode}; +use salvo::{ + async_trait, handler, serve_static::StaticDir, Depot, FlowCtrl, Handler, Request, Response, + Router, +}; +use server_fn::{Payload, ServerFunctionRegistry}; +use tokio::task::spawn_blocking; -/// Convert a salvo websocket into a LiveViewSocket -/// -/// This is required to launch a LiveView app using the warp web framework -pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { - ws.map(transform_rx) - .with(transform_tx) - .sink_map_err(|_| LiveViewError::SendingFailed) +use crate::{ + dioxus_ssr_html, + serve::ServeConfig, + server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, +}; + +pub trait DioxusRouterExt { + fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self; } -fn transform_rx(message: Result) -> Result { - let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; +impl DioxusRouterExt for Router { + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + let mut router = self; + for server_fn_path in DioxusServerFnRegistry::paths_registered() { + let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + let full_route = format!("{server_fn_route}/{server_fn_path}"); + router = router.push(Router::with_path(&full_route).post(ServerFnHandler { + server_context: DioxusServerContext {}, + function: func, + })); + } + router + } - let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self { + // Serve the dist folder and the index.html file + let serve_dir = StaticDir::new(["dist"]); - Ok(msg) + self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + .push(Router::with_path("/").get(SSRHandler { cfg })) + .push(Router::with_path("<**path>").get(serve_dir)) + } } -async fn transform_tx(message: String) -> Result { - Ok(Message::text(message)) +struct SSRHandler { + cfg: ServeConfig

, +} + +#[async_trait] +impl Handler for SSRHandler

{ + async fn handle( + &self, + _req: &mut Request, + _depot: &mut Depot, + res: &mut Response, + _flow: &mut FlowCtrl, + ) { + res.write_body(dioxus_ssr_html(&self.cfg)).unwrap(); + } +} + +struct ServerFnHandler { + server_context: DioxusServerContext, + function: Arc, +} + +#[handler] +impl ServerFnHandler { + async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) { + let Self { + server_context, + function, + } = self; + + let body = hyper::body::to_bytes(req.body_mut().unwrap()).await; + let Ok(body)=body else { + handle_error(body.err().unwrap(), res); + return; + }; + let headers = req.headers(); + + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + let function = function.clone(); + let server_context = server_context.clone(); + spawn_blocking({ + move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let resp = function(server_context, &body).await; + + resp_tx.send(resp).unwrap(); + }) + } + }); + let result = resp_rx.await.unwrap(); + + match result { + Ok(serialized) => { + // if this is Accept: application/json then send a serialized JSON response + let accept_header = headers.get("Accept").and_then(|value| value.to_str().ok()); + if accept_header == Some("application/json") + || accept_header + == Some( + "application/\ + x-www-form-urlencoded", + ) + || accept_header == Some("application/cbor") + { + res.set_status_code(StatusCode::OK); + } + + match serialized { + Payload::Binary(data) => { + res.headers_mut() + .insert("Content-Type", HeaderValue::from_static("application/cbor")); + res.write_body(data).unwrap(); + } + Payload::Url(data) => { + res.headers_mut().insert( + "Content-Type", + HeaderValue::from_static( + "application/\ + x-www-form-urlencoded", + ), + ); + res.render(data); + } + Payload::Json(data) => { + res.headers_mut() + .insert("Content-Type", HeaderValue::from_static("application/json")); + res.render(data); + } + } + } + Err(err) => handle_error(err, res), + } + } +} + +fn handle_error(error: impl Error + Send + Sync, res: &mut Response) { + let mut resp_err = Response::new(); + resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR); + resp_err.render(format!("Internal Server Error: {}", error)); + *res = resp_err; } From 0b80d32d187b6fe75352029a62366045e33b35c1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 30 Mar 2023 20:50:58 -0500 Subject: [PATCH 19/87] fix axum adapter --- packages/server/src/adapters/axum_adapter.rs | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index b7cc85e1c..9d99a697d 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -18,7 +18,10 @@ use crate::{ pub trait DioxusRouterExt { fn register_server_fns(self, server_fn_route: &'static str) -> Self; - fn serve_dioxus_application(self, cfg: ServeConfig) -> Self; + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self; } impl DioxusRouterExt for Router { @@ -37,18 +40,24 @@ impl DioxusRouterExt for Router { router } - fn serve_dioxus_application(self, cfg: ServeConfig) -> Self { + fn serve_dioxus_application( + self, + cfg: ServeConfig

, + ) -> Self { use tower_http::services::ServeDir; // Serve the dist folder and the index.html file let serve_dir = ServeDir::new("dist"); self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) - .route("/", get(move || { - let rendered = dioxus_ssr_html(cfg); - async move { Full::from(rendered) } - })) - .fallback_service( serve_dir) + .route( + "/", + get(move || { + let rendered = dioxus_ssr_html(&cfg); + async move { Full::from(rendered) } + }), + ) + .fallback_service(serve_dir) } } From 976d4ab960e0dd5d0ac84c690add93da7508a147 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 31 Mar 2023 09:40:58 -0500 Subject: [PATCH 20/87] Add warp adapter --- Cargo.toml | 1 + packages/server/Cargo.toml | 3 +- .../examples/warp-hello-world/.gitignore | 2 + .../examples/warp-hello-world/Cargo.toml | 20 +++ .../examples/warp-hello-world/src/main.rs | 65 ++++++++ packages/server/src/adapters/mod.rs | 2 + packages/server/src/adapters/salvo_adapter.rs | 4 +- packages/server/src/adapters/warp_adapter.rs | 150 +++++++++++++++--- packages/server/src/lib.rs | 2 + 9 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 packages/server/examples/warp-hello-world/.gitignore create mode 100644 packages/server/examples/warp-hello-world/Cargo.toml create mode 100644 packages/server/examples/warp-hello-world/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index bf2b4b8c4..f4ad80086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "packages/server/server-macro", "packages/server/examples/axum-hello-world", "packages/server/examples/salvo-hello-world", + "packages/server/examples/warp-hello-world", "docs/guide", ] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index fc5f33c7f..ee0a826e9 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -11,6 +11,7 @@ server_macro = { path = "server-macro" } # warp warp = { version = "0.3.3", optional = true } +http-body = { version = "0.4.5", optional = true } # axum axum = { version = "0.6.1", optional = true } @@ -31,7 +32,7 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } [features] default = [] -warp = ["dep:warp"] +warp = ["dep:warp", "http-body"] axum = ["dep:axum", "tower-http", "hyper"] salvo = ["dep:salvo", "hyper"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] diff --git a/packages/server/examples/warp-hello-world/.gitignore b/packages/server/examples/warp-hello-world/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/warp-hello-world/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml new file mode 100644 index 000000000..122eddf36 --- /dev/null +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "warp-hello-world" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-server = { path = "../../" } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tracing-subscriber = "0.3.16" +tracing = "0.1.37" +warp = { version = "0.3.3", optional = true } + +[features] +ssr = ["warp", "tokio", "dioxus-server/ssr", "dioxus-server/warp"] +web = ["dioxus-web"] diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs new file mode 100644 index 000000000..2bac390e2 --- /dev/null +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -0,0 +1,65 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let routes = serve_dioxus_application( + ServeConfig::new(app, ()).head(r#"Hello World!"#), + ); + warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index 41bb4a403..0c292bd8d 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -2,3 +2,5 @@ pub mod axum_adapter; #[cfg(feature = "salvo")] pub mod salvo_adapter; +#[cfg(feature = "warp")] +pub mod warp_adapter; diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 20edc9364..f381e0b4a 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -132,12 +132,12 @@ impl ServerFnHandler { x-www-form-urlencoded", ), ); - res.render(data); + res.write_body(data).unwrap(); } Payload::Json(data) => { res.headers_mut() .insert("Content-Type", HeaderValue::from_static("application/json")); - res.render(data); + res.write_body(data).unwrap(); } } } diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index e5c821ce3..2577de61f 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -1,28 +1,136 @@ -use crate::{LiveViewError, LiveViewSocket}; -use futures_util::{SinkExt, StreamExt}; -use warp::ws::{Message, WebSocket}; +use std::{error::Error, sync::Arc}; -/// Convert a warp websocket into a LiveViewSocket -/// -/// This is required to launch a LiveView app using the warp web framework -pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { - ws.map(transform_rx) - .with(transform_tx) - .sink_map_err(|_| LiveViewError::SendingFailed) +use server_fn::{Payload, ServerFunctionRegistry}; +use tokio::task::spawn_blocking; +use warp::{ + filters::BoxedFilter, + http::{Response, StatusCode}, + hyper::{body::Bytes, HeaderMap}, + path, Filter, Reply, +}; + +use crate::{ + dioxus_ssr_html, + serve::ServeConfig, + server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, +}; + +pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { + let mut filter: Option> = None; + for server_fn_path in DioxusServerFnRegistry::paths_registered() { + let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); + let full_route = format!("{server_fn_route}/{server_fn_path}") + .trim_start_matches('/') + .to_string(); + let route = path(full_route) + .and(warp::post()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and_then(move |headers: HeaderMap, body| { + let func = func.clone(); + async move { server_fn_handler(DioxusServerContext {}, func, headers, body).await } + }) + .boxed(); + if let Some(boxed_filter) = filter.take() { + filter = Some(boxed_filter.or(route).unify().boxed()); + } else { + filter = Some(route.boxed()); + } + } + filter.expect("No server functions found") } -fn transform_rx(message: Result) -> Result { - // destructure the message into the buffer we got from warp - let msg = message - .map_err(|_| LiveViewError::SendingFailed)? - .into_bytes(); +pub fn serve_dioxus_application( + cfg: ServeConfig

, +) -> BoxedFilter<(impl Reply,)> { + // Serve the dist folder and the index.html file + let serve_dir = warp::fs::dir("./dist"); - // transform it back into a string, saving us the allocation - let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; - - Ok(msg) + register_server_fns(cfg.server_fn_route.unwrap_or_default()) + .or(warp::path::end() + .and(warp::get()) + .map(move || warp::reply::html(dioxus_ssr_html(&cfg)))) + .or(serve_dir) + .boxed() } -async fn transform_tx(message: String) -> Result { - Ok(Message::text(message)) +#[derive(Debug)] +struct FailedToReadBody(String); + +impl warp::reject::Reject for FailedToReadBody {} + +#[derive(Debug)] +struct RecieveFailed(String); + +impl warp::reject::Reject for RecieveFailed {} + +async fn server_fn_handler( + server_context: DioxusServerContext, + function: Arc, + headers: HeaderMap, + body: Bytes, +) -> Result, warp::Rejection> { + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + spawn_blocking({ + move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async { + let resp = match function(server_context, &body).await { + Ok(serialized) => { + // if this is Accept: application/json then send a serialized JSON response + let accept_header = + headers.get("Accept").and_then(|value| value.to_str().ok()); + let mut res = Response::builder(); + if accept_header == Some("application/json") + || accept_header + == Some( + "application/\ + x-www-form-urlencoded", + ) + || accept_header == Some("application/cbor") + { + res = res.status(StatusCode::OK); + } + + let resp = match serialized { + Payload::Binary(data) => res + .header("Content-Type", "application/cbor") + .body(Bytes::from(data)), + Payload::Url(data) => res + .header( + "Content-Type", + "application/\ + x-www-form-urlencoded", + ) + .body(Bytes::from(data)), + Payload::Json(data) => res + .header("Content-Type", "application/json") + .body(Bytes::from(data)), + }; + + Box::new(resp.unwrap()) + } + Err(e) => report_err(e), + }; + + if resp_tx.send(resp).is_err() { + eprintln!("Error sending response"); + } + }) + } + }); + resp_rx.await.map_err(|err| { + warp::reject::custom(RecieveFailed(format!("Failed to recieve response {err}"))) + }) +} + +fn report_err(e: E) -> Box { + Box::new( + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!("Error: {}", e)) + .unwrap(), + ) as Box } diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index e7559363a..1cdd2f786 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -11,6 +11,8 @@ pub mod prelude { pub use crate::adapters::axum_adapter::*; #[cfg(feature = "salvo")] pub use crate::adapters::salvo_adapter::*; + #[cfg(feature = "warp")] + pub use crate::adapters::warp_adapter::*; #[cfg(feature = "ssr")] pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; From bfcb0f6eab33ed8f9ec2d9a9f0b36de51f257f70 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 31 Mar 2023 15:33:44 -0500 Subject: [PATCH 21/87] add an example illistrating intigration with the router --- Cargo.toml | 1 + packages/router/src/components/router.rs | 1 + packages/server/Cargo.toml | 2 + .../examples/axum-hello-world/Cargo.toml | 5 +- .../examples/axum-hello-world/src/main.rs | 2 + .../server/examples/axum-router/.gitignore | 2 + .../server/examples/axum-router/Cargo.toml | 21 +++ .../server/examples/axum-router/src/main.rs | 141 ++++++++++++++++++ .../examples/salvo-hello-world/Cargo.toml | 2 - .../examples/warp-hello-world/Cargo.toml | 2 - packages/server/src/adapters/axum_adapter.rs | 17 ++- packages/server/src/lib.rs | 53 +------ packages/server/src/render.rs | 92 ++++++++++++ packages/server/src/serve.rs | 8 - packages/ssr/src/renderer.rs | 6 +- 15 files changed, 283 insertions(+), 72 deletions(-) create mode 100644 packages/server/examples/axum-router/.gitignore create mode 100644 packages/server/examples/axum-router/Cargo.toml create mode 100644 packages/server/examples/axum-router/src/main.rs create mode 100644 packages/server/src/render.rs diff --git a/Cargo.toml b/Cargo.toml index f4ad80086..e2e427043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "packages/server/examples/axum-hello-world", "packages/server/examples/salvo-hello-world", "packages/server/examples/warp-hello-world", + "packages/server/examples/axum-router", "docs/guide", ] diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 54131a13a..b027386c5 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -29,6 +29,7 @@ pub struct RouterProps<'a> { pub active_class: Option<&'a str>, /// Set the initial url. + #[props(!optional, into)] pub initial_url: Option, } diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index ee0a826e9..fa0e0592a 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -17,6 +17,7 @@ http-body = { version = "0.4.5", optional = true } axum = { version = "0.6.1", optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs"] } hyper = { version = "0.14.25", optional = true } +axum-macros = "0.3.7" # salvo salvo = { version = "0.37.7", optional = true, features = ["serve-static"] } @@ -29,6 +30,7 @@ log = "0.4.17" once_cell = "1.17.1" thiserror = "1.0.40" tokio = { version = "1.27.0", features = ["full"], optional = true } +object-pool = "0.5.4" [features] default = [] diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index 54aaf17d3..68da7a0be 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -8,13 +8,12 @@ edition = "2021" [dependencies] dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } dioxus = { path = "../../../dioxus" } +dioxus-router = { path = "../../../router" } dioxus-server = { path = "../../" } axum = { version = "0.6.12", optional = true } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" [features] ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum"] -web = ["dioxus-web"] +web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 4d4ab5e78..b4bc76390 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -25,7 +25,9 @@ fn main() { axum::Router::new() .serve_dioxus_application( ServeConfig::new(app, ()).head(r#"Hello World!"#), + None, ) + .with_state(SSRState::default()) .into_make_service(), ) .await diff --git a/packages/server/examples/axum-router/.gitignore b/packages/server/examples/axum-router/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/axum-router/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml new file mode 100644 index 000000000..d1d8ff81d --- /dev/null +++ b/packages/server/examples/axum-router/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "axum-router" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-router = { path = "../../../router" } +dioxus-server = { path = "../../" } +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" +tower-http = { version = "0.4.0", features = ["fs"], optional = true } +http = { version = "0.2.9", optional = true } + +[features] +ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum", "tower-http", "http"] +web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs new file mode 100644 index 000000000..8745aa7f1 --- /dev/null +++ b/packages/server/examples/axum-router/src/main.rs @@ -0,0 +1,141 @@ +//! Run with: +//! +//! ```sh +//! dioxus build --features web +//! cargo run --features ssr +//! ``` + +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_router::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_with_props( + App, + AppProps { route: None }, + dioxus_web::Config::new().hydrate(true), + ); + #[cfg(feature = "ssr")] + { + use axum::extract::State; + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + use tower_http::services::ServeDir; + + // Serve the dist/assets folder with the javascript and WASM files created by the CLI + let serve_dir = ServeDir::new("dist/assets"); + + axum::Server::bind(&addr) + .serve( + axum::Router::new() + // Register server functions + .register_server_fns("") + // Serve the static assets folder + .nest_service("/assets", serve_dir) + // If the path is unknown, render the application + .fallback( + move |uri: http::uri::Uri, State(ssr_state): State| { + let rendered = ssr_state.render( + &ServeConfig::new( + App, + AppProps { + route: Some(format!("http://{addr}{uri}")), + }, + ) + .head(r#"Hello World!"#), + ); + async move { axum::body::Full::from(rendered) } + }, + ) + .with_state(SSRState::default()) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +#[derive(Clone, Debug, Props, PartialEq)] +struct AppProps { + route: Option, +} + +fn App(cx: Scope) -> Element { + cx.render(rsx! { + Router { + initial_url: cx.props.route.clone(), + + Route { to: "/blog", + Link { + to: "/", + "Go to counter" + } + table { + tbody { + for _ in 0..100 { + tr { + for _ in 0..100 { + td { "hello world??" } + } + } + } + } + } + }, + // Fallback + Route { to: "", + Counter {} + }, + } + }) +} + +fn Counter(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + Link { + to: "/blog", + "Go to blog" + } + div{ + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + } + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 1ebe56dfc..9c81ee2a2 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -11,8 +11,6 @@ dioxus = { path = "../../../dioxus" } dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" salvo = { version = "0.37.9", optional = true } [features] diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index 122eddf36..7bb9aa23f 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -11,8 +11,6 @@ dioxus = { path = "../../../dioxus" } dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" -tracing-subscriber = "0.3.16" -tracing = "0.1.37" warp = { version = "0.3.3", optional = true } [features] diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 9d99a697d..5eaff9bfc 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,6 +2,7 @@ use std::{error::Error, sync::Arc}; use axum::{ body::{self, Body, BoxBody, Full}, + extract::{FromRef, State}, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, @@ -11,7 +12,7 @@ use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ - dioxus_ssr_html, + render::SSRState, serve::ServeConfig, server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, }; @@ -21,10 +22,15 @@ pub trait DioxusRouterExt { fn serve_dioxus_application( self, cfg: ServeConfig

, + server_fn_route: Option<&'static str>, ) -> Self; } -impl DioxusRouterExt for Router { +impl DioxusRouterExt for Router +where + SSRState: FromRef, + S: Send + Sync + Clone + 'static, +{ fn register_server_fns(self, server_fn_route: &'static str) -> Self { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { @@ -43,17 +49,18 @@ impl DioxusRouterExt for Router { fn serve_dioxus_application( self, cfg: ServeConfig

, + server_fn_route: Option<&'static str>, ) -> Self { use tower_http::services::ServeDir; // Serve the dist folder and the index.html file let serve_dir = ServeDir::new("dist"); - self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + self.register_server_fns(server_fn_route.unwrap_or_default()) .route( "/", - get(move || { - let rendered = dioxus_ssr_html(&cfg); + get(move |State(ssr_state): State| { + let rendered = ssr_state.render(&cfg); async move { Full::from(rendered) } }), ) diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 1cdd2f786..9141c7199 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -3,6 +3,8 @@ use dioxus_core::prelude::*; mod adapters; #[cfg(feature = "ssr")] +pub mod render; +#[cfg(feature = "ssr")] mod serve; mod server_fn; @@ -14,57 +16,10 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(feature = "ssr")] + pub use crate::render::*; + #[cfg(feature = "ssr")] pub use crate::serve::ServeConfig; pub use crate::server_fn::{DioxusServerContext, ServerFn}; pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_macro::*; } - -#[cfg(feature = "ssr")] -fn dioxus_ssr_html(cfg: &serve::ServeConfig

) -> String { - use prelude::ServeConfig; - - let ServeConfig { - app, - application_name, - base_path, - head, - props, - .. - } = cfg; - - let application_name = application_name.unwrap_or("dioxus"); - - let mut vdom = VirtualDom::new_with_props(*app, props.clone()); - let _ = vdom.rebuild(); - let renderered = dioxus_ssr::pre_render(&vdom); - let base_path = base_path.unwrap_or("."); - let head = head.unwrap_or( - r#"Dioxus Application - - - "#, - ); - format!( - r#" - - - - {head} - - -

- {renderered} -
- - -"# - ) -} diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs new file mode 100644 index 000000000..5c15bd097 --- /dev/null +++ b/packages/server/src/render.rs @@ -0,0 +1,92 @@ +use std::fmt::Write; +use std::sync::Arc; + +use dioxus_core::VirtualDom; +use dioxus_ssr::Renderer; + +use crate::prelude::ServeConfig; + +fn dioxus_ssr_html(cfg: &ServeConfig

, renderer: &mut Renderer) -> String { + let ServeConfig { + app, + application_name, + base_path, + head, + props, + .. + } = cfg; + + let application_name = application_name.unwrap_or("dioxus"); + let mut vdom = VirtualDom::new_with_props(*app, props.clone()); + let _ = vdom.rebuild(); + let base_path = base_path.unwrap_or("."); + let head = head.unwrap_or( + r#"Dioxus Application + + + "#, + ); + + let mut html = String::new(); + + if let Err(err) = write!( + &mut html, + r#" + + + {head} + + +

"# + ) { + eprintln!("Failed to write to html: {}", err); + } + + let _ = renderer.render_to(&mut html, &vdom); + + if let Err(err) = write!( + &mut html, + r#"
+ + + "# + ) { + eprintln!("Failed to write to html: {}", err); + } + + html +} + +#[derive(Clone)] +pub struct SSRState { + // We keep a cache of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move + renderers: Arc>, +} + +impl Default for SSRState { + fn default() -> Self { + Self { + renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)), + } + } +} + +impl SSRState { + pub fn render(&self, cfg: &ServeConfig

) -> String { + let mut renderer = self.renderers.pull(pre_renderer); + dioxus_ssr_html(cfg, &mut renderer) + } +} + +fn pre_renderer() -> Renderer { + let mut renderer = Renderer::default(); + renderer.pre_render = true; + renderer +} diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs index 9cca17a6b..65f0871ae 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve.rs @@ -5,7 +5,6 @@ pub struct ServeConfig { pub(crate) app: Component

, pub(crate) props: P, pub(crate) application_name: Option<&'static str>, - pub(crate) server_fn_route: Option<&'static str>, pub(crate) base_path: Option<&'static str>, pub(crate) head: Option<&'static str>, } @@ -17,7 +16,6 @@ impl ServeConfig

{ app, props, application_name: None, - server_fn_route: None, base_path: None, head: None, } @@ -29,12 +27,6 @@ impl ServeConfig

{ self } - /// Set the base route all server functions will be served under - pub fn server_fn_route(mut self, server_fn_route: &'static str) -> Self { - self.server_fn_route = Some(server_fn_route); - self - } - /// Set the path the WASM application will be served under pub fn base_path(mut self, base_path: &'static str) -> Self { self.base_path = Some(base_path); diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 5b6fa8fb2..d9c312ac7 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -3,7 +3,7 @@ use crate::cache::StringCache; use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn}; use std::collections::HashMap; use std::fmt::Write; -use std::rc::Rc; +use std::sync::Arc; /// A virtualdom renderer that caches the templates it has seen for faster rendering #[derive(Default)] @@ -25,7 +25,7 @@ pub struct Renderer { pub skip_components: bool, /// A cache of templates that have been rendered - template_cache: HashMap<&'static str, Rc>, + template_cache: HashMap<&'static str, Arc>, } impl Renderer { @@ -67,7 +67,7 @@ impl Renderer { let entry = self .template_cache .entry(template.template.get().name) - .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap())) + .or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap())) .clone(); for segment in entry.segments.iter() { From 5ffdb4dbede3e8806566a0debd69d0442c8521a9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 31 Mar 2023 16:16:47 -0500 Subject: [PATCH 22/87] prefech wasm + JS by default --- .../server/examples/axum-router/src/main.rs | 15 ++++----- packages/server/src/render.rs | 31 +++++++++++++------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs index 8745aa7f1..0ae68d50e 100644 --- a/packages/server/examples/axum-router/src/main.rs +++ b/packages/server/examples/axum-router/src/main.rs @@ -41,15 +41,12 @@ fn main() { // If the path is unknown, render the application .fallback( move |uri: http::uri::Uri, State(ssr_state): State| { - let rendered = ssr_state.render( - &ServeConfig::new( - App, - AppProps { - route: Some(format!("http://{addr}{uri}")), - }, - ) - .head(r#"Hello World!"#), - ); + let rendered = ssr_state.render(&ServeConfig::new( + App, + AppProps { + route: Some(format!("http://{addr}{uri}")), + }, + )); async move { axum::body::Full::from(rendered) } }, ) diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 5c15bd097..872a6cffc 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -20,25 +20,36 @@ fn dioxus_ssr_html(cfg: &ServeConfig

, renderer: &mut Rend let mut vdom = VirtualDom::new_with_props(*app, props.clone()); let _ = vdom.rebuild(); let base_path = base_path.unwrap_or("."); - let head = head.unwrap_or( - r#"Dioxus Application - - - "#, - ); let mut html = String::new(); - if let Err(err) = write!( - &mut html, - r#" + let result = match head { + Some(head) => { + write!( + &mut html, + r#" {head}

"# - ) { + ) + } + None => { + write!( + &mut html, + r#"Dioxus Application + + + + + "# + ) + } + }; + + if let Err(err) = result { eprintln!("Failed to write to html: {}", err); } From 71ddd50963b9aae55386ba64936d217a9bf6b5b7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 1 Apr 2023 17:00:09 -0500 Subject: [PATCH 23/87] provide a nicer builder API --- .../examples/axum-hello-world/src/main.rs | 6 +-- .../server/examples/axum-router/src/main.rs | 17 ++++--- .../examples/salvo-hello-world/src/main.rs | 5 +- .../examples/warp-hello-world/src/main.rs | 4 +- packages/server/src/render.rs | 28 ++--------- packages/server/src/serve.rs | 47 ++++++++++++++++++- 6 files changed, 64 insertions(+), 43 deletions(-) diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index b4bc76390..fc07c6fbb 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -23,11 +23,7 @@ fn main() { axum::Server::bind(&addr) .serve( axum::Router::new() - .serve_dioxus_application( - ServeConfig::new(app, ()).head(r#"Hello World!"#), - None, - ) - .with_state(SSRState::default()) + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) .into_make_service(), ) .await diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs index 0ae68d50e..53193859c 100644 --- a/packages/server/examples/axum-router/src/main.rs +++ b/packages/server/examples/axum-router/src/main.rs @@ -41,12 +41,15 @@ fn main() { // If the path is unknown, render the application .fallback( move |uri: http::uri::Uri, State(ssr_state): State| { - let rendered = ssr_state.render(&ServeConfig::new( - App, - AppProps { - route: Some(format!("http://{addr}{uri}")), - }, - )); + let rendered = ssr_state.render( + &ServeConfigBuilder::new( + App, + AppProps { + route: Some(format!("http://{addr}{uri}")), + }, + ) + .build(), + ); async move { axum::body::Full::from(rendered) } }, ) @@ -79,7 +82,7 @@ fn App(cx: Scope) -> Element { for _ in 0..100 { tr { for _ in 0..100 { - td { "hello world??" } + td { "hello world!" } } } } diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index cd8b80c51..f13668c5e 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -20,9 +20,8 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let router = Router::new().serve_dioxus_application( - ServeConfig::new(app, ()).head(r#"Hello World!"#), - ); + let router = + Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ())); Server::new(TcpListener::bind("127.0.0.1:8080")) .serve(router) .await; diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 2bac390e2..87f91782a 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -19,9 +19,7 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let routes = serve_dioxus_application( - ServeConfig::new(app, ()).head(r#"Hello World!"#), - ); + let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ())); warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; }); } diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 872a6cffc..cb752eaed 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -16,38 +16,20 @@ fn dioxus_ssr_html(cfg: &ServeConfig

, renderer: &mut Rend .. } = cfg; - let application_name = application_name.unwrap_or("dioxus"); let mut vdom = VirtualDom::new_with_props(*app, props.clone()); let _ = vdom.rebuild(); - let base_path = base_path.unwrap_or("."); let mut html = String::new(); - let result = match head { - Some(head) => { - write!( - &mut html, - r#" + let result = write!( + &mut html, + r#" {head} - - +

"# - ) - } - None => { - write!( - &mut html, - r#"Dioxus Application - - - - - "# - ) - } - }; + ); if let Err(err) = result { eprintln!("Failed to write to html: {}", err); diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs index 65f0871ae..ed059f47f 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve.rs @@ -1,15 +1,16 @@ use dioxus_core::Component; #[derive(Clone)] -pub struct ServeConfig { +pub struct ServeConfigBuilder { pub(crate) app: Component

, pub(crate) props: P, pub(crate) application_name: Option<&'static str>, pub(crate) base_path: Option<&'static str>, pub(crate) head: Option<&'static str>, + pub(crate) assets_path: Option<&'static str>, } -impl ServeConfig

{ +impl ServeConfigBuilder

{ /// Create a new ServeConfig pub fn new(app: Component

, props: P) -> Self { Self { @@ -18,6 +19,7 @@ impl ServeConfig

{ application_name: None, base_path: None, head: None, + assets_path: None, } } @@ -38,4 +40,45 @@ impl ServeConfig

{ self.head = Some(head); self } + + /// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist/assets) + pub fn assets_path(mut self, assets_path: &'static str) -> Self { + self.assets_path = Some(assets_path); + self + } + + /// Build the ServeConfig + pub fn build(self) -> ServeConfig

{ + let base_path = self.base_path.unwrap_or("."); + let application_name = self.application_name.unwrap_or("dioxus"); + ServeConfig { + app: self.app, + props: self.props, + application_name, + base_path, + head: self.head.map(String::from).unwrap_or(format!(r#"Dioxus Application + + + + + "#)), + assets_path: self.assets_path.unwrap_or("dist/assets"), + } + } +} + +#[derive(Clone)] +pub struct ServeConfig { + pub(crate) app: Component

, + pub(crate) props: P, + pub(crate) application_name: &'static str, + pub(crate) base_path: &'static str, + pub(crate) head: String, + pub(crate) assets_path: &'static str, +} + +impl From> for ServeConfig

{ + fn from(builder: ServeConfigBuilder

) -> Self { + builder.build() + } } From c6992c7032474eaeda4bb8371cf64d22dada2981 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 1 Apr 2023 17:00:12 -0500 Subject: [PATCH 24/87] make server function API more flexible --- packages/server/Cargo.toml | 3 +- packages/server/server-macro/Cargo.toml | 3 +- packages/server/server-macro/src/lib.rs | 14 ++-- packages/server/src/adapters/axum_adapter.rs | 81 +++++++++++++------ packages/server/src/adapters/salvo_adapter.rs | 69 ++++++++++++---- packages/server/src/adapters/warp_adapter.rs | 69 +++++++++++----- packages/server/src/lib.rs | 8 +- packages/server/src/server_context.rs | 35 ++++++++ packages/server/src/server_fn.rs | 3 +- 9 files changed, 209 insertions(+), 76 deletions(-) create mode 100644 packages/server/src/server_context.rs diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index fa0e0592a..2485a2529 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -server_fn = { version = "0.2.4", features = ["stable"] } +server_fn = { path = "D:/Users/Desktop/github/leptos/server_fn", features = ["stable"] } server_macro = { path = "server-macro" } # warp @@ -31,6 +31,7 @@ once_cell = "1.17.1" thiserror = "1.0.40" tokio = { version = "1.27.0", features = ["full"], optional = true } object-pool = "0.5.4" +anymap = "0.12.1" [features] default = [] diff --git a/packages/server/server-macro/Cargo.toml b/packages/server/server-macro/Cargo.toml index cb285f517..e5fb6523e 100644 --- a/packages/server/server-macro/Cargo.toml +++ b/packages/server/server-macro/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" [dependencies] quote = "1.0.26" -server_fn_macro = { version = "0.2.4", features = ["stable"] } +# server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "1e037ecb60965c7c55fd781fdc8de7863ffd102b", features = ["stable"] } +server_fn_macro = { path = "D:/Users/Desktop/github/leptos/server_fn_macro", features = ["stable"] } syn = { version = "1", features = ["full"] } [lib] diff --git a/packages/server/server-macro/src/lib.rs b/packages/server/server-macro/src/lib.rs index f2a2e1665..f6b4f92a2 100644 --- a/packages/server/server-macro/src/lib.rs +++ b/packages/server/server-macro/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::ToTokens; use server_fn_macro::*; -/// Declares that a function is a [server function](leptos_server). This means that +/// Declares that a function is a [server function](dioxus_server). This means that /// its body will only run on the server, i.e., when the `ssr` feature is enabled. /// /// If you call a server function from the client (i.e., when the `csr` or `hydrate` features @@ -19,18 +19,18 @@ use server_fn_macro::*; /// work without WebAssembly, the encoding must be `"Url"`. /// /// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope), +/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext), /// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other /// server-side context into the server function. /// /// ```ignore -/// # use leptos::*; use serde::{Serialize, Deserialize}; +/// # use dioxus_server::prelude::*; use serde::{Serialize, Deserialize}; /// # #[derive(Serialize, Deserialize)] /// # pub struct Post { } /// #[server(ReadPosts, "/api")] /// pub async fn read_posts(how_many: u8, query: String) -> Result, ServerFnError> { /// // do some work on the server to access the database -/// todo!() +/// todo!() /// } /// ``` /// @@ -42,7 +42,7 @@ use server_fn_macro::*; /// - **Server functions must return `Result`.** Even if the work being done /// inside the function body can’t fail, the processes of serialization/deserialization and the /// network call are fallible. -/// - **Return types must be [Serializable](leptos_reactive::Serializable).** +/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).** /// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) @@ -50,8 +50,8 @@ use server_fn_macro::*; /// They are serialized as an `application/x-www-form-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 +/// - **The [DioxusServerContext](dioxus_server::prelude::DioxusServerContext) comes from the server.** Optionally, the first argument of a server function +/// can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext). 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. #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 5eaff9bfc..8fb98b6ec 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,7 +2,8 @@ use std::{error::Error, sync::Arc}; use axum::{ body::{self, Body, BoxBody, Full}, - extract::{FromRef, State}, + extract::State, + handler::Handler, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, @@ -14,61 +15,89 @@ use tokio::task::spawn_blocking; use crate::{ render::SSRState, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_context::DioxusServerContext, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; -pub trait DioxusRouterExt { +pub trait DioxusRouterExt { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + handler: impl Fn(Arc) -> H, + ) -> Self + where + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static; fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn serve_dioxus_application( self, - cfg: ServeConfig

, - server_fn_route: Option<&'static str>, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self; } -impl DioxusRouterExt for Router +impl DioxusRouterExt for Router where - SSRState: FromRef, S: Send + Sync + Clone + 'static, { - fn register_server_fns(self, server_fn_route: &'static str) -> Self { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + mut handler: impl FnMut(Arc) -> H, + ) -> Self + where + H: Handler, + T: 'static, + S: Clone + Send + Sync + 'static, + { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.route( - &full_route, - post(move |headers: HeaderMap, body: Request| async move { - server_fn_handler(DioxusServerContext {}, func.clone(), headers, body).await - }), - ); + router = router.route(&full_route, post(handler(func))); } router } + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + self.register_server_fns_with_handler(server_fn_route, |func| { + move |headers: HeaderMap, body: Request| async move { + server_fn_handler(DioxusServerContext::default(), func.clone(), headers, body).await + } + }) + } + fn serve_dioxus_application( self, - cfg: ServeConfig

, - server_fn_route: Option<&'static str>, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self { use tower_http::services::ServeDir; - // Serve the dist folder and the index.html file - let serve_dir = ServeDir::new("dist"); + let cfg = cfg.into(); - self.register_server_fns(server_fn_route.unwrap_or_default()) - .route( + // Serve the dist folder and the index.html file + let serve_dir = ServeDir::new(cfg.assets_path); + + self.register_server_fns(server_fn_route) + .nest_service("/assets", serve_dir) + .route_service( "/", - get(move |State(ssr_state): State| { - let rendered = ssr_state.render(&cfg); - async move { Full::from(rendered) } - }), + get(render_handler).with_state((cfg, SSRState::default())), ) - .fallback_service(serve_dir) } } -async fn server_fn_handler( +async fn render_handler( + State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, +) -> impl IntoResponse { + let rendered = ssr_state.render(&cfg); + Full::from(rendered) +} + +pub async fn server_fn_handler( server_context: DioxusServerContext, function: Arc, headers: HeaderMap, diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index f381e0b4a..0d8a27b22 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -9,43 +9,65 @@ use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ - dioxus_ssr_html, + prelude::DioxusServerContext, + prelude::SSRState, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; pub trait DioxusRouterExt { fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + handler: impl Fn(Arc) -> H, + ) -> Self + where + H: Handler + 'static; fn serve_dioxus_application( self, - cfg: ServeConfig

, + server_fn_path: &'static str, + cfg: impl Into>, ) -> Self; } impl DioxusRouterExt for Router { - fn register_server_fns(self, server_fn_route: &'static str) -> Self { + fn register_server_fns_with_handler( + self, + server_fn_route: &'static str, + mut handler: impl FnMut(Arc) -> H, + ) -> Self + where + H: Handler + 'static, + { let mut router = self; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.push(Router::with_path(&full_route).post(ServerFnHandler { - server_context: DioxusServerContext {}, - function: func, - })); + router = router.push(Router::with_path(&full_route).post(handler(func))); } router } + fn register_server_fns(self, server_fn_route: &'static str) -> Self { + self.register_server_fns_with_handler(|| ServerFnHandler { + server_context: DioxusServerContext::default(), + function: func, + }) + } + fn serve_dioxus_application( self, - cfg: ServeConfig

, + server_fn_route: &'static str, + cfg: impl Into>, ) -> Self { + let cfg = cfg.into(); // Serve the dist folder and the index.html file - let serve_dir = StaticDir::new(["dist"]); + let serve_dir = StaticDir::new([cfg.assets_path]); - self.register_server_fns(cfg.server_fn_route.unwrap_or_default()) + self.register_server_fns(server_fn_route) .push(Router::with_path("/").get(SSRHandler { cfg })) - .push(Router::with_path("<**path>").get(serve_dir)) + .push(Router::with_path("assets/<**path>").get(serve_dir)) } } @@ -58,19 +80,36 @@ impl Handler for SSRHandler

{ async fn handle( &self, _req: &mut Request, - _depot: &mut Depot, + depot: &mut Depot, res: &mut Response, _flow: &mut FlowCtrl, ) { - res.write_body(dioxus_ssr_html(&self.cfg)).unwrap(); + // Get the SSR renderer from the depot or create a new one if it doesn't exist + let renderer_pool = if let Some(renderer) = depot.obtain::() { + renderer.clone() + } else { + let renderer = SSRState::default(); + depot.inject(renderer.clone()); + renderer + }; + res.write_body(renderer_pool.render(&self.cfg)).unwrap(); } } -struct ServerFnHandler { +pub struct ServerFnHandler { server_context: DioxusServerContext, function: Arc, } +impl ServerFnHandler { + pub fn new(server_context: DioxusServerContext, function: Arc) -> Self { + Self { + server_context, + function, + } + } +} + #[handler] impl ServerFnHandler { async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) { diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 2577de61f..00ebb38fa 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -10,50 +10,75 @@ use warp::{ }; use crate::{ - dioxus_ssr_html, + prelude::{DioxusServerContext, SSRState}, serve::ServeConfig, - server_fn::{DioxusServerContext, DioxusServerFnRegistry, ServerFnTraitObj}, + server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; -pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { - let mut filter: Option> = None; +pub fn register_server_fns_with_handler( + server_fn_route: &'static str, + mut handler: H, +) -> BoxedFilter<(R,)> +where + H: FnMut(String, Arc) -> F, + F: Filter + Send + Sync + 'static, + F::Extract: Send, + R: Reply + 'static, +{ + let mut filter: Option> = None; for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}") .trim_start_matches('/') .to_string(); - let route = path(full_route) - .and(warp::post()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(move |headers: HeaderMap, body| { - let func = func.clone(); - async move { server_fn_handler(DioxusServerContext {}, func, headers, body).await } - }) - .boxed(); + let route = handler(full_route, func.clone()).boxed(); if let Some(boxed_filter) = filter.take() { filter = Some(boxed_filter.or(route).unify().boxed()); } else { - filter = Some(route.boxed()); + filter = Some(route); } } filter.expect("No server functions found") } -pub fn serve_dioxus_application( - cfg: ServeConfig

, -) -> BoxedFilter<(impl Reply,)> { - // Serve the dist folder and the index.html file - let serve_dir = warp::fs::dir("./dist"); +pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { + register_server_fns_with_handler(server_fn_route, |full_route, func| { + path(full_route) + .and(warp::post()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and_then(move |headers: HeaderMap, body| { + let func = func.clone(); + async move { + server_fn_handler(DioxusServerContext::default(), func, headers, body).await + } + }) + }) +} - register_server_fns(cfg.server_fn_route.unwrap_or_default()) +pub fn serve_dioxus_application( + server_fn_route: &'static str, + cfg: impl Into>, +) -> BoxedFilter<(impl Reply,)> { + let cfg = cfg.into(); + // Serve the dist folder and the index.html file + let serve_dir = warp::fs::dir(cfg.assets_path); + + register_server_fns(server_fn_route) .or(warp::path::end() .and(warp::get()) - .map(move || warp::reply::html(dioxus_ssr_html(&cfg)))) - .or(serve_dir) + .and(with_ssr_state()) + .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg)))) + .or(warp::path("assets").and(serve_dir)) .boxed() } +fn with_ssr_state() -> impl Filter + Clone +{ + let renderer = SSRState::default(); + warp::any().map(move || renderer.clone()) +} + #[derive(Debug)] struct FailedToReadBody(String); diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 9141c7199..a9c8e4939 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -6,6 +6,7 @@ mod adapters; pub mod render; #[cfg(feature = "ssr")] mod serve; +mod server_context; mod server_fn; pub mod prelude { @@ -18,8 +19,11 @@ pub mod prelude { #[cfg(feature = "ssr")] pub use crate::render::*; #[cfg(feature = "ssr")] - pub use crate::serve::ServeConfig; - pub use crate::server_fn::{DioxusServerContext, ServerFn}; + pub use crate::serve::{ServeConfig, ServeConfigBuilder}; + pub use crate::server_context::DioxusServerContext; + pub use crate::server_fn::ServerFn; + #[cfg(feature = "ssr")] + pub use crate::server_fn::ServerFnTraitObj; pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_macro::*; } diff --git a/packages/server/src/server_context.rs b/packages/server/src/server_context.rs new file mode 100644 index 000000000..427ee21f8 --- /dev/null +++ b/packages/server/src/server_context.rs @@ -0,0 +1,35 @@ +use std::sync::{Arc, PoisonError, RwLock, RwLockWriteGuard}; + +use anymap::{any::Any, Map}; + +type SendSyncAnyMap = Map; + +/// A shared context for server functions. This allows you to pass data between your server and the server functions like authentication session data. +#[derive(Clone)] +pub struct DioxusServerContext { + shared_context: Arc>, +} + +impl Default for DioxusServerContext { + fn default() -> Self { + Self { + shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())), + } + } +} + +impl DioxusServerContext { + pub fn get(&self) -> Option { + self.shared_context.read().ok()?.get::().cloned() + } + + pub fn insert( + &mut self, + value: T, + ) -> Result<(), PoisonError>> { + self.shared_context + .write() + .map(|mut map| map.insert(value)) + .map(|_| ()) + } +} diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 2f000adda..7170d61de 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -1,5 +1,4 @@ -#[derive(Clone)] -pub struct DioxusServerContext {} +use crate::server_context::DioxusServerContext; #[cfg(any(feature = "ssr", doc))] pub type ServerFnTraitObj = server_fn::ServerFnTraitObj; From 6a51f8998d8c012454122e4b7afbb84d2238d1f3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 1 Apr 2023 17:05:44 -0500 Subject: [PATCH 25/87] simplify server package features --- packages/server/Cargo.toml | 6 +++--- packages/server/examples/axum-hello-world/Cargo.toml | 3 ++- packages/server/examples/axum-hello-world/src/main.rs | 2 +- packages/server/examples/axum-router/Cargo.toml | 3 ++- packages/server/examples/axum-router/src/main.rs | 2 +- packages/server/examples/salvo-hello-world/Cargo.toml | 3 ++- packages/server/examples/salvo-hello-world/src/main.rs | 2 +- packages/server/examples/warp-hello-world/Cargo.toml | 3 ++- packages/server/examples/warp-hello-world/src/main.rs | 2 +- 9 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 2485a2529..5828875be 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -35,7 +35,7 @@ anymap = "0.12.1" [features] default = [] -warp = ["dep:warp", "http-body"] -axum = ["dep:axum", "tower-http", "hyper"] -salvo = ["dep:salvo", "hyper"] +warp = ["dep:warp", "http-body", "ssr"] +axum = ["dep:axum", "tower-http", "hyper", "ssr"] +salvo = ["dep:salvo", "hyper", "ssr"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index 68da7a0be..ed466e6e1 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -15,5 +15,6 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" [features] -ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum"] +default = ["web"] +ssr = ["axum", "tokio", "dioxus-server/axum"] web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index fc07c6fbb..899247af5 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -2,7 +2,7 @@ //! //! ```sh //! dioxus build --features web -//! cargo run --features ssr +//! cargo run --features ssr --no-default-features //! ``` #![allow(non_snake_case)] diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml index d1d8ff81d..297aa6726 100644 --- a/packages/server/examples/axum-router/Cargo.toml +++ b/packages/server/examples/axum-router/Cargo.toml @@ -17,5 +17,6 @@ tower-http = { version = "0.4.0", features = ["fs"], optional = true } http = { version = "0.2.9", optional = true } [features] -ssr = ["axum", "tokio", "dioxus-server/ssr", "dioxus-server/axum", "tower-http", "http"] +default = ["web"] +ssr = ["axum", "tokio", "dioxus-server/axum", "tower-http", "http"] web = ["dioxus-web", "dioxus-router/web"] diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs index 53193859c..2afa4a481 100644 --- a/packages/server/examples/axum-router/src/main.rs +++ b/packages/server/examples/axum-router/src/main.rs @@ -2,7 +2,7 @@ //! //! ```sh //! dioxus build --features web -//! cargo run --features ssr +//! cargo run --features ssr --no-default-features //! ``` #![allow(non_snake_case)] diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 9c81ee2a2..4633eaafc 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -14,5 +14,6 @@ serde = "1.0.159" salvo = { version = "0.37.9", optional = true } [features] -ssr = ["salvo", "tokio", "dioxus-server/ssr", "dioxus-server/salvo"] +default = ["web"] +ssr = ["salvo", "tokio", "dioxus-server/salvo"] web = ["dioxus-web"] diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index f13668c5e..d1e654fee 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -2,7 +2,7 @@ //! //! ```sh //! dioxus build --features web -//! cargo run --features ssr +//! cargo run --features ssr --no-default-features //! ``` #![allow(non_snake_case)] diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index 7bb9aa23f..86ac2da5b 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -14,5 +14,6 @@ serde = "1.0.159" warp = { version = "0.3.3", optional = true } [features] -ssr = ["warp", "tokio", "dioxus-server/ssr", "dioxus-server/warp"] +default = ["web"] +ssr = ["warp", "tokio", "dioxus-server/warp"] web = ["dioxus-web"] diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 87f91782a..51a627d16 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -2,7 +2,7 @@ //! //! ```sh //! dioxus build --features web -//! cargo run --features ssr +//! cargo run --features ssr --no-default-features //! ``` #![allow(non_snake_case)] From f96425e4259ab2b0b881af0faf1f3ecbd3da1dd2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 2 Apr 2023 15:07:51 -0500 Subject: [PATCH 26/87] collect templates for hot reloading --- packages/server/Cargo.toml | 8 ++- packages/server/src/render.rs | 114 ++++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 5828875be..086448ca1 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# server functions server_fn = { path = "D:/Users/Desktop/github/leptos/server_fn", features = ["stable"] } server_macro = { path = "server-macro" } @@ -23,6 +24,7 @@ axum-macros = "0.3.7" salvo = { version = "0.37.7", optional = true, features = ["serve-static"] } serde = "1.0.159" +# Dioxus + SSR dioxus-core = { path = "../core", version = "^0.3.0" } dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true } @@ -33,8 +35,12 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } object-pool = "0.5.4" anymap = "0.12.1" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dioxus-hot-reload = { path = "../hot-reload" } + [features] -default = [] +default = ["hot-reload"] +hot-reload = [] warp = ["dep:warp", "http-body", "ssr"] axum = ["dep:axum", "tower-http", "hyper", "ssr"] salvo = ["dep:salvo", "hyper", "ssr"] diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index cb752eaed..bf8c12854 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -6,40 +6,82 @@ use dioxus_ssr::Renderer; use crate::prelude::ServeConfig; -fn dioxus_ssr_html(cfg: &ServeConfig

, renderer: &mut Renderer) -> String { - let ServeConfig { - app, - application_name, - base_path, - head, - props, - .. - } = cfg; +#[derive(Clone)] +pub struct SSRState { + // We keep a cache of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move + renderers: Arc>, + #[cfg(all(debug_assertions, feature = "hot-reload"))] + // The cache of all templates that have been modified since the last time we checked + templates: Arc>>>, +} - let mut vdom = VirtualDom::new_with_props(*app, props.clone()); - let _ = vdom.rebuild(); +impl Default for SSRState { + fn default() -> Self { + #[cfg(all(debug_assertions, feature = "hot-reload"))] + let templates = { + let templates = Arc::new(std::sync::RwLock::new(std::collections::HashSet::new())); + dioxus_hot_reload::connect({ + let templates = templates.clone(); + move |msg| match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + if let Ok(mut templates) = templates.write() { + templates.insert(template); + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + std::process::exit(0); + } + } + }); + templates + }; - let mut html = String::new(); + Self { + renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)), + #[cfg(all(debug_assertions, feature = "hot-reload"))] + templates, + } + } +} - let result = write!( - &mut html, - r#" +impl SSRState { + pub fn render(&self, cfg: &ServeConfig

) -> String { + let ServeConfig { + app, + application_name, + base_path, + head, + props, + .. + } = cfg; + + let mut vdom = VirtualDom::new_with_props(*app, props.clone()); + + let _ = vdom.rebuild(); + + let mut renderer = self.renderers.pull(pre_renderer); + + let mut html = String::new(); + + let result = write!( + &mut html, + r#" {head}

"# - ); + ); - if let Err(err) = result { - eprintln!("Failed to write to html: {}", err); - } + if let Err(err) = result { + eprintln!("Failed to write to html: {}", err); + } - let _ = renderer.render_to(&mut html, &vdom); + let _ = renderer.render_to(&mut html, &vdom); - if let Err(err) = write!( - &mut html, - r#"
+ if let Err(err) = write!( + &mut html, + r#"
"# - ) { - eprintln!("Failed to write to html: {}", err); - } - - html -} - -#[derive(Clone)] -pub struct SSRState { - // We keep a cache of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move - renderers: Arc>, -} - -impl Default for SSRState { - fn default() -> Self { - Self { - renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)), + ) { + eprintln!("Failed to write to html: {}", err); } - } -} -impl SSRState { - pub fn render(&self, cfg: &ServeConfig

) -> String { - let mut renderer = self.renderers.pull(pre_renderer); - dioxus_ssr_html(cfg, &mut renderer) + html } } From 9877dd7ed8928a2c5d08b546a54c93b26204c287 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 2 Apr 2023 16:18:15 -0500 Subject: [PATCH 27/87] parse and inject pre rendered content to work with trunk --- packages/server/src/adapters/axum_adapter.rs | 48 ++++++++-- packages/server/src/adapters/salvo_adapter.rs | 36 +++++++- packages/server/src/adapters/warp_adapter.rs | 2 +- packages/server/src/render.rs | 39 +------- packages/server/src/serve.rs | 92 ++++++++++++------- 5 files changed, 132 insertions(+), 85 deletions(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 8fb98b6ec..75c736731 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -70,23 +70,51 @@ where } fn serve_dioxus_application( - self, + mut self, server_fn_route: &'static str, cfg: impl Into>, ) -> Self { - use tower_http::services::ServeDir; + use tower_http::services::{ServeDir, ServeFile}; let cfg = cfg.into(); - // Serve the dist folder and the index.html file - let serve_dir = ServeDir::new(cfg.assets_path); - - self.register_server_fns(server_fn_route) - .nest_service("/assets", serve_dir) - .route_service( - "/", - get(render_handler).with_state((cfg, SSRState::default())), + // Serve all files in dist folder except index.html + let dir = std::fs::read_dir(cfg.assets_path).unwrap_or_else(|e| { + panic!( + "Couldn't read assets directory at {:?}: {}", + &cfg.assets_path, e ) + }); + + for entry in dir.flatten() { + let path = entry.path(); + if path.ends_with("index.html") { + continue; + } + let route = path + .strip_prefix(&cfg.assets_path) + .unwrap() + .iter() + .map(|segment| { + segment.to_str().unwrap_or_else(|| { + panic!("Failed to convert path segment {:?} to string", segment) + }) + }) + .collect::>() + .join("/"); + let route = format!("/{}", route); + if path.is_dir() { + self = self.nest_service(&route, ServeDir::new(path)); + } else { + self = self.nest_service(&route, ServeFile::new(path)); + } + } + + // Add server functions and render index.html + self.register_server_fns(server_fn_route).route( + "/", + get(render_handler).with_state((cfg, SSRState::default())), + ) } } diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 0d8a27b22..f3bbd5f43 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -50,24 +50,50 @@ impl DioxusRouterExt for Router { } fn register_server_fns(self, server_fn_route: &'static str) -> Self { - self.register_server_fns_with_handler(|| ServerFnHandler { + self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler { server_context: DioxusServerContext::default(), function: func, }) } fn serve_dioxus_application( - self, + mut self, server_fn_route: &'static str, cfg: impl Into>, ) -> Self { let cfg = cfg.into(); - // Serve the dist folder and the index.html file - let serve_dir = StaticDir::new([cfg.assets_path]); + + // Serve all files in dist folder except index.html + let dir = std::fs::read_dir(cfg.assets_path).unwrap_or_else(|e| { + panic!( + "Couldn't read assets directory at {:?}: {}", + &cfg.assets_path, e + ) + }); + + for entry in dir.flatten() { + let path = entry.path(); + if path.ends_with("index.html") { + continue; + } + let serve_dir = StaticDir::new([path.clone()]); + let route = path + .strip_prefix(&cfg.assets_path) + .unwrap() + .iter() + .map(|segment| { + segment.to_str().unwrap_or_else(|| { + panic!("Failed to convert path segment {:?} to string", segment) + }) + }) + .collect::>() + .join("/"); + let route = format!("/{}/<**path>", route); + self = self.push(Router::with_path(route).get(serve_dir)) + } self.register_server_fns(server_fn_route) .push(Router::with_path("/").get(SSRHandler { cfg })) - .push(Router::with_path("assets/<**path>").get(serve_dir)) } } diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 00ebb38fa..82d1c382f 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -69,7 +69,7 @@ pub fn serve_dioxus_application( .and(warp::get()) .and(with_ssr_state()) .map(move |renderer: SSRState| warp::reply::html(renderer.render(&cfg)))) - .or(warp::path("assets").and(serve_dir)) + .or(serve_dir) .boxed() } diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index bf8c12854..f6582d6a7 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -1,4 +1,3 @@ -use std::fmt::Write; use std::sync::Arc; use dioxus_core::VirtualDom; @@ -47,12 +46,7 @@ impl Default for SSRState { impl SSRState { pub fn render(&self, cfg: &ServeConfig

) -> String { let ServeConfig { - app, - application_name, - base_path, - head, - props, - .. + app, props, index, .. } = cfg; let mut vdom = VirtualDom::new_with_props(*app, props.clone()); @@ -63,38 +57,11 @@ impl SSRState { let mut html = String::new(); - let result = write!( - &mut html, - r#" - - - {head} - -

"# - ); - - if let Err(err) = result { - eprintln!("Failed to write to html: {}", err); - } + html += &index.pre_main; let _ = renderer.render_to(&mut html, &vdom); - if let Err(err) = write!( - &mut html, - r#"
- - - "# - ) { - eprintln!("Failed to write to html: {}", err); - } + html += &index.post_main; html } diff --git a/packages/server/src/serve.rs b/packages/server/src/serve.rs index ed059f47f..6284a6f1b 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve.rs @@ -1,12 +1,15 @@ +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + use dioxus_core::Component; #[derive(Clone)] pub struct ServeConfigBuilder { pub(crate) app: Component

, pub(crate) props: P, - pub(crate) application_name: Option<&'static str>, - pub(crate) base_path: Option<&'static str>, - pub(crate) head: Option<&'static str>, + pub(crate) root_id: Option<&'static str>, + pub(crate) index_path: Option<&'static str>, pub(crate) assets_path: Option<&'static str>, } @@ -16,32 +19,25 @@ impl ServeConfigBuilder

{ Self { app, props, - application_name: None, - base_path: None, - head: None, + root_id: None, + index_path: None, assets_path: None, } } - /// Set the application name matching the name in the Dioxus.toml file used to build the application - pub fn application_name(mut self, application_name: &'static str) -> Self { - self.application_name = Some(application_name); + /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html) + pub fn index_path(mut self, index_path: &'static str) -> Self { + self.index_path = Some(index_path); self } - /// Set the path the WASM application will be served under - pub fn base_path(mut self, base_path: &'static str) -> Self { - self.base_path = Some(base_path); + /// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main) + pub fn root_id(mut self, root_id: &'static str) -> Self { + self.root_id = Some(root_id); self } - /// Set the head content to be included in the HTML document served - pub fn head(mut self, head: &'static str) -> Self { - self.head = Some(head); - self - } - - /// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist/assets) + /// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist) pub fn assets_path(mut self, assets_path: &'static str) -> Self { self.assets_path = Some(assets_path); self @@ -49,31 +45,61 @@ impl ServeConfigBuilder

{ /// Build the ServeConfig pub fn build(self) -> ServeConfig

{ - let base_path = self.base_path.unwrap_or("."); - let application_name = self.application_name.unwrap_or("dioxus"); + let assets_path = self.assets_path.unwrap_or("dist"); + + let index_path = self + .index_path + .map(PathBuf::from) + .unwrap_or_else(|| format!("{assets_path}/index.html").into()); + + let root_id = self.root_id.unwrap_or("main"); + + let index = load_index_html(index_path, root_id); + ServeConfig { app: self.app, props: self.props, - application_name, - base_path, - head: self.head.map(String::from).unwrap_or(format!(r#"Dioxus Application - - - - - "#)), - assets_path: self.assets_path.unwrap_or("dist/assets"), + index, + assets_path, } } } +fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml { + let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built."); + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .expect("Failed to read index.html"); + + let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page.")); + + let post_main = post_main.split_once('>').unwrap_or_else(|| { + panic!("Failed to find closing > after id=\"{root_id}\" in index.html.") + }); + + let (pre_main, post_main) = ( + pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">", + post_main.1.to_string(), + ); + + IndexHtml { + pre_main, + post_main, + } +} + +#[derive(Clone)] +pub(crate) struct IndexHtml { + pub(crate) pre_main: String, + pub(crate) post_main: String, +} + #[derive(Clone)] pub struct ServeConfig { pub(crate) app: Component

, pub(crate) props: P, - pub(crate) application_name: &'static str, - pub(crate) base_path: &'static str, - pub(crate) head: String, + pub(crate) index: IndexHtml, pub(crate) assets_path: &'static str, } From 7214130c4040873c3f1ee17ded1d5afb8a01f6c7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 2 Apr 2023 17:45:28 -0500 Subject: [PATCH 28/87] hot reloading intigration --- packages/server/Cargo.toml | 10 ++- packages/server/src/adapters/axum_adapter.rs | 71 +++++++++++++-- packages/server/src/adapters/salvo_adapter.rs | 87 ++++++++++++++++++- packages/server/src/adapters/warp_adapter.rs | 69 ++++++++++++++- packages/server/src/hot_reload.rs | 46 ++++++++++ packages/server/src/lib.rs | 2 + packages/server/src/render.rs | 24 ----- 7 files changed, 274 insertions(+), 35 deletions(-) create mode 100644 packages/server/src/hot_reload.rs diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 086448ca1..563912754 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -15,13 +15,13 @@ warp = { version = "0.3.3", optional = true } http-body = { version = "0.4.5", optional = true } # axum -axum = { version = "0.6.1", optional = true } +axum = { version = "0.6.1", features = ["ws"], optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs"] } hyper = { version = "0.14.25", optional = true } axum-macros = "0.3.7" # salvo -salvo = { version = "0.37.7", optional = true, features = ["serve-static"] } +salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] } serde = "1.0.159" # Dioxus + SSR @@ -35,12 +35,16 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } object-pool = "0.5.4" anymap = "0.12.1" +serde_json = { version = "1.0.95", optional = true } +tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } +futures-util = { version = "0.3.28", optional = true } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { path = "../hot-reload" } [features] default = ["hot-reload"] -hot-reload = [] +hot-reload = ["serde_json", "tokio-stream", "futures-util"] warp = ["dep:warp", "http-body", "ssr"] axum = ["dep:axum", "tower-http", "hyper", "ssr"] salvo = ["dep:salvo", "hyper", "ssr"] diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 75c736731..9fbcd5893 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,7 +2,7 @@ use std::{error::Error, sync::Arc}; use axum::{ body::{self, Body, BoxBody, Full}, - extract::State, + extract::{State, WebSocketUpgrade}, handler::Handler, http::{HeaderMap, Request, Response, StatusCode}, response::IntoResponse, @@ -36,6 +36,8 @@ pub trait DioxusRouterExt { server_fn_route: &'static str, cfg: impl Into>, ) -> Self; + + fn connect_hot_reload(self) -> Self; } impl DioxusRouterExt for Router @@ -92,7 +94,7 @@ where continue; } let route = path - .strip_prefix(&cfg.assets_path) + .strip_prefix(cfg.assets_path) .unwrap() .iter() .map(|segment| { @@ -111,10 +113,26 @@ where } // Add server functions and render index.html - self.register_server_fns(server_fn_route).route( - "/", - get(render_handler).with_state((cfg, SSRState::default())), - ) + self.connect_hot_reload() + .register_server_fns(server_fn_route) + .route( + "/", + get(render_handler).with_state((cfg, SSRState::default())), + ) + } + + fn connect_hot_reload(self) -> Self { + #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] + { + self.route( + "/_dioxus/hot_reload", + get(hot_reload_handler).with_state(crate::hot_reload::HotReloadState::default()), + ) + } + #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] + { + self + } } } @@ -195,3 +213,44 @@ fn report_err(e: E) -> Response { .body(body::boxed(format!("Error: {}", e))) .unwrap() } + +#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] +pub async fn hot_reload_handler( + ws: WebSocketUpgrade, + State(state): State, +) -> impl IntoResponse { + use axum::extract::ws::Message; + use futures_util::StreamExt; + + ws.on_upgrade(|mut socket| async move { + println!("🔥 Hot Reload WebSocket connected"); + { + // update any rsx calls that changed before the websocket connected. + { + println!("🔮 Finding updates since last compile..."); + let templates_read = state.templates.read().await; + + for template in &*templates_read { + if socket + .send(Message::Text(serde_json::to_string(&template).unwrap())) + .await + .is_err() + { + return; + } + } + } + println!("finished"); + } + + let mut rx = tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver); + while let Some(change) = rx.next().await { + if let Some(template) = change { + let template = { serde_json::to_string(&template).unwrap() }; + if socket.send(Message::Text(template)).await.is_err() { + break; + }; + } + } + }) +} diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index f3bbd5f43..24aab9643 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -17,6 +17,7 @@ use crate::{ pub trait DioxusRouterExt { fn register_server_fns(self, server_fn_route: &'static str) -> Self; + fn register_server_fns_with_handler( self, server_fn_route: &'static str, @@ -24,11 +25,14 @@ pub trait DioxusRouterExt { ) -> Self where H: Handler + 'static; + fn serve_dioxus_application( self, server_fn_path: &'static str, cfg: impl Into>, ) -> Self; + + fn connect_hot_reload(self) -> Self; } impl DioxusRouterExt for Router { @@ -92,9 +96,14 @@ impl DioxusRouterExt for Router { self = self.push(Router::with_path(route).get(serve_dir)) } - self.register_server_fns(server_fn_route) + self.connect_hot_reload() + .register_server_fns(server_fn_route) .push(Router::with_path("/").get(SSRHandler { cfg })) } + + fn connect_hot_reload(self) -> Self { + self.push(Router::with_path("/_dioxus/hot_reload").get(HotReloadHandler::default())) + } } struct SSRHandler { @@ -217,3 +226,79 @@ fn handle_error(error: impl Error + Send + Sync, res: &mut Response) { resp_err.render(format!("Internal Server Error: {}", error)); *res = resp_err; } + +#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] +#[derive(Default)] +pub struct HotReloadHandler; + +#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] +#[handler] +impl HotReloadHandler { + async fn handle( + &self, + _req: &mut Request, + _depot: &mut Depot, + _res: &mut Response, + ) -> Result<(), salvo::http::StatusError> { + Err(salvo::http::StatusError::not_found()) + } +} + +#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] +#[derive(Default)] +pub struct HotReloadHandler { + state: crate::hot_reload::HotReloadState, +} + +#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] +#[handler] +impl HotReloadHandler { + async fn handle( + &self, + req: &mut Request, + _depot: &mut Depot, + res: &mut Response, + ) -> Result<(), salvo::http::StatusError> { + use salvo::ws::Message; + use salvo::ws::WebSocketUpgrade; + + let state = self.state.clone(); + + WebSocketUpgrade::new() + .upgrade(req, res, |mut websocket| async move { + use futures_util::StreamExt; + + println!("🔥 Hot Reload WebSocket connected"); + { + // update any rsx calls that changed before the websocket connected. + { + println!("🔮 Finding updates since last compile..."); + let templates_read = state.templates.read().await; + + for template in &*templates_read { + if websocket + .send(Message::text(serde_json::to_string(&template).unwrap())) + .await + .is_err() + { + return; + } + } + } + println!("finished"); + } + + let mut rx = + tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver); + while let Some(change) = rx.next().await { + if let Some(template) = change { + let template = { serde_json::to_string(&template).unwrap() }; + if websocket.send(Message::text(template)).await.is_err() { + break; + }; + } + } + }) + .await + } +} diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 82d1c382f..67c3c75e4 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -64,7 +64,8 @@ pub fn serve_dioxus_application( // Serve the dist folder and the index.html file let serve_dir = warp::fs::dir(cfg.assets_path); - register_server_fns(server_fn_route) + connect_hot_reload() + .or(register_server_fns(server_fn_route)) .or(warp::path::end() .and(warp::get()) .and(with_ssr_state()) @@ -159,3 +160,69 @@ fn report_err(e: E) -> Box { .unwrap(), ) as Box } + +pub fn connect_hot_reload() -> impl Filter { + #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] + { + warp::path("_dioxus/hot_reload").and(warp::ws()).map(|| { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body("Not Found".into()) + .unwrap() + }) + } + #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] + { + use crate::hot_reload::HotReloadState; + let state = HotReloadState::default(); + + warp::path("_dioxus") + .and(warp::path("hot_reload")) + .and(warp::ws()) + .and(warp::any().map(move || state.clone())) + .map(move |ws: warp::ws::Ws, state: HotReloadState| { + #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] + ws.on_upgrade(move |mut websocket| { + async move { + use futures_util::sink::SinkExt; + use futures_util::StreamExt; + use warp::ws::Message; + + println!("🔥 Hot Reload WebSocket connected"); + { + // update any rsx calls that changed before the websocket connected. + { + println!("🔮 Finding updates since last compile..."); + let templates_read = state.templates.read().await; + + for template in &*templates_read { + if websocket + .send(Message::text( + serde_json::to_string(&template).unwrap(), + )) + .await + .is_err() + { + return; + } + } + } + println!("finished"); + } + + let mut rx = tokio_stream::wrappers::WatchStream::from_changes( + state.message_receiver, + ); + while let Some(change) = rx.next().await { + if let Some(template) = change { + let template = { serde_json::to_string(&template).unwrap() }; + if websocket.send(Message::text(template)).await.is_err() { + break; + }; + } + } + } + }) + }) + } +} diff --git a/packages/server/src/hot_reload.rs b/packages/server/src/hot_reload.rs new file mode 100644 index 000000000..c4f89e704 --- /dev/null +++ b/packages/server/src/hot_reload.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use dioxus_core::Template; +use tokio::sync::{ + watch::{channel, Receiver}, + RwLock, +}; + +#[derive(Clone)] +pub struct HotReloadState { + // The cache of all templates that have been modified since the last time we checked + pub(crate) templates: Arc>>>, + // The channel to send messages to the hot reload thread + pub(crate) message_receiver: Receiver>>, +} + +impl Default for HotReloadState { + fn default() -> Self { + let templates = Arc::new(RwLock::new(std::collections::HashSet::new())); + let (tx, rx) = channel(None); + + dioxus_hot_reload::connect({ + let templates = templates.clone(); + move |msg| match msg { + dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { + { + let mut templates = templates.blocking_write(); + templates.insert(template); + } + + if let Err(err) = tx.send(Some(template)) { + log::error!("Failed to send hot reload message: {}", err); + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + std::process::exit(0); + } + } + }); + + Self { + templates, + message_receiver: rx, + } + } +} diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index a9c8e4939..5e395fa70 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -2,6 +2,8 @@ use dioxus_core::prelude::*; mod adapters; +#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] +mod hot_reload; #[cfg(feature = "ssr")] pub mod render; #[cfg(feature = "ssr")] diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index f6582d6a7..1f66067fd 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -9,36 +9,12 @@ use crate::prelude::ServeConfig; pub struct SSRState { // We keep a cache of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move renderers: Arc>, - #[cfg(all(debug_assertions, feature = "hot-reload"))] - // The cache of all templates that have been modified since the last time we checked - templates: Arc>>>, } impl Default for SSRState { fn default() -> Self { - #[cfg(all(debug_assertions, feature = "hot-reload"))] - let templates = { - let templates = Arc::new(std::sync::RwLock::new(std::collections::HashSet::new())); - dioxus_hot_reload::connect({ - let templates = templates.clone(); - move |msg| match msg { - dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { - if let Ok(mut templates) = templates.write() { - templates.insert(template); - } - } - dioxus_hot_reload::HotReloadMsg::Shutdown => { - std::process::exit(0); - } - } - }); - templates - }; - Self { renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)), - #[cfg(all(debug_assertions, feature = "hot-reload"))] - templates, } } } From 987a0d5532c721675789334cec46994cdc0f5433 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 2 Apr 2023 18:13:27 -0500 Subject: [PATCH 29/87] fix salvo serving static files --- packages/server/src/adapters/salvo_adapter.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 24aab9643..357e1c19c 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -2,8 +2,9 @@ use std::{error::Error, sync::Arc}; use hyper::{http::HeaderValue, StatusCode}; use salvo::{ - async_trait, handler, serve_static::StaticDir, Depot, FlowCtrl, Handler, Request, Response, - Router, + async_trait, handler, + serve_static::{StaticDir, StaticFile}, + Depot, FlowCtrl, Handler, Request, Response, Router, }; use server_fn::{Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; @@ -80,7 +81,6 @@ impl DioxusRouterExt for Router { if path.ends_with("index.html") { continue; } - let serve_dir = StaticDir::new([path.clone()]); let route = path .strip_prefix(&cfg.assets_path) .unwrap() @@ -92,8 +92,15 @@ impl DioxusRouterExt for Router { }) .collect::>() .join("/"); - let route = format!("/{}/<**path>", route); - self = self.push(Router::with_path(route).get(serve_dir)) + if path.is_file() { + let route = format!("/{}", route); + let serve_dir = StaticFile::new(path.clone()); + self = self.push(Router::with_path(route).get(serve_dir)) + } else { + let route = format!("/{}/<**path>", route); + let serve_dir = StaticDir::new([path.clone()]); + self = self.push(Router::with_path(route).get(serve_dir)) + } } self.connect_hot_reload() From d05c85db31d7e592bf91cc68589e2548b1767245 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 08:09:22 -0500 Subject: [PATCH 30/87] Document common items --- packages/server/src/adapters/mod.rs | 12 ++++ packages/server/src/lib.rs | 72 +++++++++++++++++-- packages/server/src/render.rs | 7 +- .../server/src/{serve.rs => serve_config.rs} | 7 +- packages/server/src/server_context.rs | 39 +++++++++- packages/server/src/server_fn.rs | 10 ++- 6 files changed, 132 insertions(+), 15 deletions(-) rename packages/server/src/{serve.rs => serve_config.rs} (85%) diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index 0c292bd8d..148062db0 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -1,3 +1,15 @@ +//! # Adapters +//! Adapters for different web frameworks. +//! +//! Each adapter provides a set of utilities that is ergonomic to use with the framework. +//! +//! Each framework has utilies for some or all of the following: +//! - Server functions +//! - A generic way to register server functions +//! - A way to register server functions with a custom handler that allows users to pass in a custom [`DioxusServerContext`] based on the state of the server framework. +//! - A way to register static WASM files that is accepts [`ServeConfig`] +//! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload) + #[cfg(feature = "axum")] pub mod axum_adapter; #[cfg(feature = "salvo")] diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 5e395fa70..9654adebe 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,3 +1,66 @@ +//! Fullstack utilities for the [`dioxus`](https://dioxuslabs.com) framework. +//! +//! # Features +//! - Intigrations with the [axum](crate::adapters::axum_adapter), [salvo](crate::adapters::salvo_adapters), and [warp](crate::adapters::warp_adapters) server frameworks with utilities for serving and rendering Dioxus applications. +//! - Server functions that allow you to call code on the server from the client as if it were a normal function. +//! - Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload). +//! +//! # Example +//! ```rust +//! #![allow(non_snake_case)] +//! use dioxus::prelude::*; +//! use dioxus_server::prelude::*; +//! +//! fn main() { +//! #[cfg(feature = "web")] +//! // Hydrate the application on the client +//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); +//! #[cfg(feature = "ssr")] +//! { +//! GetServerData::register().unwrap(); +//! tokio::runtime::Runtime::new() +//! .unwrap() +//! .block_on(async move { +//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); +//! axum::Server::bind(&addr) +//! .serve( +//! axum::Router::new() +//! // Server side render the application, serve static assets, and register server functions +//! .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) +//! .into_make_service(), +//! ) +//! .await +//! .unwrap(); +//! }); +//! } +//! } +//! +//! fn app(cx: Scope) -> Element { +//! let text = use_state(cx, || "...".to_string()); +//! +//! cx.render(rsx! { +//! button { +//! onclick: move |_| { +//! to_owned![text]; +//! async move { +//! if let Ok(data) = get_server_data().await { +//! text.set(data.clone()); +//! } +//! } +//! }, +//! "Run a server function" +//! } +//! "Server said: {text}" +//! }) +//! } +//! +//! #[server(GetServerData)] +//! async fn get_server_data() -> Result { +//! Ok("Hello from the server!".to_string()) +//! } +//! ``` + +#![warn(missing_docs)] #[allow(unused)] use dioxus_core::prelude::*; @@ -5,12 +68,13 @@ mod adapters; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; #[cfg(feature = "ssr")] -pub mod render; +mod render; #[cfg(feature = "ssr")] -mod serve; +mod serve_config; mod server_context; mod server_fn; +/// A prelude of commonly used items in dioxus-server. pub mod prelude { #[cfg(feature = "axum")] pub use crate::adapters::axum_adapter::*; @@ -19,9 +83,7 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(feature = "ssr")] - pub use crate::render::*; - #[cfg(feature = "ssr")] - pub use crate::serve::{ServeConfig, ServeConfigBuilder}; + pub use crate::serve_config::{ServeConfig, ServeConfigBuilder}; pub use crate::server_context::DioxusServerContext; pub use crate::server_fn::ServerFn; #[cfg(feature = "ssr")] diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 1f66067fd..22e751ea8 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -1,3 +1,5 @@ +//! A shared pool of renderers for efficient server side rendering. + use std::sync::Arc; use dioxus_core::VirtualDom; @@ -5,9 +7,10 @@ use dioxus_ssr::Renderer; use crate::prelude::ServeConfig; +/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders. #[derive(Clone)] -pub struct SSRState { - // We keep a cache of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move +pub(crate) struct SSRState { + // We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move renderers: Arc>, } diff --git a/packages/server/src/serve.rs b/packages/server/src/serve_config.rs similarity index 85% rename from packages/server/src/serve.rs rename to packages/server/src/serve_config.rs index 6284a6f1b..b7b29e9fb 100644 --- a/packages/server/src/serve.rs +++ b/packages/server/src/serve_config.rs @@ -1,9 +1,12 @@ +//! Configeration for how to serve a Dioxus application + use std::fs::File; use std::io::Read; use std::path::PathBuf; use dioxus_core::Component; +/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`]. #[derive(Clone)] pub struct ServeConfigBuilder { pub(crate) app: Component

, @@ -14,7 +17,7 @@ pub struct ServeConfigBuilder { } impl ServeConfigBuilder

{ - /// Create a new ServeConfig + /// Create a new ServeConfigBuilder with the root component and props to render on the server. pub fn new(app: Component

, props: P) -> Self { Self { app, @@ -95,6 +98,8 @@ pub(crate) struct IndexHtml { pub(crate) post_main: String, } +/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`]. +/// See [`ServeConfigBuilder`] to create a ServeConfig #[derive(Clone)] pub struct ServeConfig { pub(crate) app: Component

, diff --git a/packages/server/src/server_context.rs b/packages/server/src/server_context.rs index 427ee21f8..7e2a494c5 100644 --- a/packages/server/src/server_context.rs +++ b/packages/server/src/server_context.rs @@ -4,7 +4,8 @@ use anymap::{any::Any, Map}; type SendSyncAnyMap = Map; -/// A shared context for server functions. This allows you to pass data between your server and the server functions like authentication session data. +/// A shared context for server functions. +/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions. #[derive(Clone)] pub struct DioxusServerContext { shared_context: Arc>, @@ -19,10 +20,12 @@ impl Default for DioxusServerContext { } impl DioxusServerContext { + /// Clone a value from the shared server context pub fn get(&self) -> Option { self.shared_context.read().ok()?.get::().cloned() } + /// Insert a value into the shared server context pub fn insert( &mut self, value: T, @@ -33,3 +36,37 @@ impl DioxusServerContext { .map(|_| ()) } } + +/// Generate a server context from a tuple of values +macro_rules! server_context { + ($({$(($name:ident: $ty:ident)),*}),*) => { + $( + #[allow(unused_mut)] + impl< $($ty: Send + Sync + 'static),* > From<($($ty,)*)> for $crate::server_context::DioxusServerContext { + fn from(( $($name,)* ): ($($ty,)*)) -> Self { + let mut context = $crate::server_context::DioxusServerContext::default(); + $(context.insert::<$ty>($name).unwrap();)* + context + } + } + )* + }; +} + +server_context!( + {}, + {(a: A)}, + {(a: A), (b: B)}, + {(a: A), (b: B), (c: C)}, + {(a: A), (b: B), (c: C), (d: D)}, + {(a: A), (b: B), (c: C), (d: D), (e: E)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L), (m: M)}, + {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L), (m: M), (n: N)} +); diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 7170d61de..6f985a39a 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -1,6 +1,7 @@ use crate::server_context::DioxusServerContext; #[cfg(any(feature = "ssr", doc))] +/// A trait object for a function that be called on serializable arguments and returns a serializable result. pub type ServerFnTraitObj = server_fn::ServerFnTraitObj; #[cfg(any(feature = "ssr", doc))] @@ -78,17 +79,14 @@ pub enum ServerRegistrationFnError { /// 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. /// -/// (This follows the same convention as the Dioxus framework's distinction between `ssr` for server-side rendering, -/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) -/// /// Server functions are created using the `server` macro. /// /// The function should be registered by calling `ServerFn::register()`. The set of server functions -/// can be queried on the server for routing purposes by calling [server_fn_by_path]. +/// can be queried on the server for routing purposes by calling [ServerFunctionRegistry::get]. /// -/// Technically, the trait is implemented on a type that describes the server function's arguments. +/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself. pub trait ServerFn: server_fn::ServerFn { - /// Registers the server function, allowing the server to query it by URL. + /// Registers the server function, allowing the client to query it by URL. #[cfg(any(feature = "ssr", doc))] fn register() -> Result<(), server_fn::ServerFnError> { Self::register_in::() From 9d5d64737803a4b2e31e55a13fe10e1204bf594d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 08:09:30 -0500 Subject: [PATCH 31/87] document axum adapter --- packages/server/src/adapters/axum_adapter.rs | 179 ++++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 9fbcd5893..ad3fafd6a 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -1,3 +1,60 @@ +//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework. +//! +//! # Example +//! ```rust +//! # #![allow(non_snake_case)] +//! # use dioxus::prelude::*; +//! # use dioxus_server::prelude::*; +//! +//! fn main() { +//! #[cfg(feature = "web")] +//! // Hydrate the application on the client +//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); +//! #[cfg(feature = "ssr")] +//! { +//! GetServerData::register().unwrap(); +//! tokio::runtime::Runtime::new() +//! .unwrap() +//! .block_on(async move { +//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); +//! axum::Server::bind(&addr) +//! .serve( +//! axum::Router::new() +//! // Server side render the application, serve static assets, and register server functions +//! .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) +//! .into_make_service(), +//! ) +//! .await +//! .unwrap(); +//! }); +//! } +//! } +//! +//! fn app(cx: Scope) -> Element { +//! let text = use_state(cx, || "...".to_string()); +//! +//! cx.render(rsx! { +//! button { +//! onclick: move |_| { +//! to_owned![text]; +//! async move { +//! if let Ok(data) = get_server_data().await { +//! text.set(data.clone()); +//! } +//! } +//! }, +//! "Run a server function" +//! } +//! "Server said: {text}" +//! }) +//! } +//! +//! #[server(GetServerData)] +//! async fn get_server_data() -> Result { +//! Ok("Hello from the server!".to_string()) +//! } +//! ``` + use std::{error::Error, sync::Arc}; use axum::{ @@ -14,12 +71,41 @@ use tokio::task::spawn_blocking; use crate::{ render::SSRState, - serve::ServeConfig, + serve_config::ServeConfig, server_context::DioxusServerContext, server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; +/// A extension trait with utilities for integrating Dioxus with your Axum router. pub trait DioxusRouterExt { + /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. + /// + /// # Example + /// ```rust + /// # use dioxus::prelude::*; + /// # use dioxus_server::prelude::*; + /// + /// fn main() { + /// tokio::runtime::Runtime::new() + /// .unwrap() + /// .block_on(async move { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// .register_server_fns_with_handler("", |func| { + /// move |headers: HeaderMap, body: Request| async move { + /// // Add the headers to the context + /// server_fn_handler((headers.clone(),).into(), func.clone(), headers, body).await + /// } + /// }) + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); + /// }); + /// } + /// ``` fn register_server_fns_with_handler( self, server_fn_route: &'static str, @@ -29,15 +115,98 @@ pub trait DioxusRouterExt { H: Handler, T: 'static, S: Clone + Send + Sync + 'static; + + /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions. + /// + /// # Example + /// ```rust + /// # use dioxus::prelude::*; + /// # use dioxus_server::prelude::*; + /// + /// fn main() { + /// tokio::runtime::Runtime::new() + /// .unwrap() + /// .block_on(async move { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// .register_server_fns("") + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); + /// }); + /// } + /// ``` fn register_server_fns(self, server_fn_route: &'static str) -> Self; + /// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`]. + /// + /// # Example + /// /// # Example + /// ```rust + /// # #![allow(non_snake_case)] + /// # use dioxus::prelude::*; + /// # use dioxus_server::prelude::*; + /// + /// fn main() { + /// GetServerData::register().unwrap(); + /// tokio::runtime::Runtime::new() + /// .unwrap() + /// .block_on(async move { + /// hot_reload_init!(); + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .connect_hot_reload() + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); + /// }); + /// } + /// + /// # fn app(cx: Scope) -> Element {todo!()} + /// ``` + fn connect_hot_reload(self) -> Self; + + /// Serves the Dioxus application. This will serve a complete server side rendered application. + /// It will serve static assets, server render the application, register server functions, and intigrate with hot reloading. + /// + /// # Example + /// ```rust + /// # #![allow(non_snake_case)] + /// # use dioxus::prelude::*; + /// # use dioxus_server::prelude::*; + /// + /// fn main() { + /// GetServerData::register().unwrap(); + /// tokio::runtime::Runtime::new() + /// .unwrap() + /// .block_on(async move { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); + /// }); + /// } + /// + /// # fn app(cx: Scope) -> Element {todo!()} + /// ``` fn serve_dioxus_application( self, server_fn_route: &'static str, cfg: impl Into>, ) -> Self; - - fn connect_hot_reload(self) -> Self; } impl DioxusRouterExt for Router @@ -66,7 +235,7 @@ where fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { move |headers: HeaderMap, body: Request| async move { - server_fn_handler(DioxusServerContext::default(), func.clone(), headers, body).await + server_fn_handler(().into(), func.clone(), headers, body).await } }) } @@ -143,6 +312,7 @@ async fn render_handler( Full::from(rendered) } +/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub async fn server_fn_handler( server_context: DioxusServerContext, function: Arc, @@ -214,6 +384,7 @@ fn report_err(e: E) -> Response { .unwrap() } +/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] pub async fn hot_reload_handler( ws: WebSocketUpgrade, From 556e4f374b1cf226e4302e4150a381990118a512 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 11:59:40 -0500 Subject: [PATCH 32/87] Document salvo adapter --- packages/server/src/adapters/axum_adapter.rs | 127 ++++++------- packages/server/src/adapters/salvo_adapter.rs | 167 +++++++++++++++++- 2 files changed, 217 insertions(+), 77 deletions(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index ad3fafd6a..34e6a5e46 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -39,7 +39,7 @@ //! to_owned![text]; //! async move { //! if let Ok(data) = get_server_data().await { -//! text.set(data.clone()); +//! text.set(data); //! } //! } //! }, @@ -85,25 +85,22 @@ pub trait DioxusRouterExt { /// # use dioxus::prelude::*; /// # use dioxus_server::prelude::*; /// - /// fn main() { - /// tokio::runtime::Runtime::new() - /// .unwrap() - /// .block_on(async move { - /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - /// axum::Server::bind(&addr) - /// .serve( - /// axum::Router::new() - /// .register_server_fns_with_handler("", |func| { - /// move |headers: HeaderMap, body: Request| async move { - /// // Add the headers to the context - /// server_fn_handler((headers.clone(),).into(), func.clone(), headers, body).await - /// } - /// }) - /// .into_make_service(), - /// ) - /// .await - /// .unwrap(); - /// }); + /// #[tokio::main] + /// async fn main() { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// .register_server_fns_with_handler("", |func| { + /// move |headers: HeaderMap, body: Request| async move { + /// // Add the headers to the context + /// server_fn_handler((headers.clone(),), func.clone(), headers, body).await + /// } + /// }) + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); /// } /// ``` fn register_server_fns_with_handler( @@ -123,20 +120,17 @@ pub trait DioxusRouterExt { /// # use dioxus::prelude::*; /// # use dioxus_server::prelude::*; /// - /// fn main() { - /// tokio::runtime::Runtime::new() - /// .unwrap() - /// .block_on(async move { - /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - /// axum::Server::bind(&addr) - /// .serve( - /// axum::Router::new() - /// .register_server_fns("") - /// .into_make_service(), - /// ) - /// .await - /// .unwrap(); - /// }); + /// #[tokio::main] + /// async fn main() { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// .register_server_fns("") + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); /// } /// ``` fn register_server_fns(self, server_fn_route: &'static str) -> Self; @@ -150,23 +144,19 @@ pub trait DioxusRouterExt { /// # use dioxus::prelude::*; /// # use dioxus_server::prelude::*; /// - /// fn main() { - /// GetServerData::register().unwrap(); - /// tokio::runtime::Runtime::new() - /// .unwrap() - /// .block_on(async move { - /// hot_reload_init!(); - /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - /// axum::Server::bind(&addr) - /// .serve( - /// axum::Router::new() - /// // Server side render the application, serve static assets, and register server functions - /// .connect_hot_reload() - /// .into_make_service(), - /// ) - /// .await - /// .unwrap(); - /// }); + /// #[tokio::main] + /// async fn main() { + /// hot_reload_init!(); + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .connect_hot_reload() + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); /// } /// /// # fn app(cx: Scope) -> Element {todo!()} @@ -174,7 +164,7 @@ pub trait DioxusRouterExt { fn connect_hot_reload(self) -> Self; /// Serves the Dioxus application. This will serve a complete server side rendered application. - /// It will serve static assets, server render the application, register server functions, and intigrate with hot reloading. + /// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading. /// /// # Example /// ```rust @@ -182,22 +172,18 @@ pub trait DioxusRouterExt { /// # use dioxus::prelude::*; /// # use dioxus_server::prelude::*; /// - /// fn main() { - /// GetServerData::register().unwrap(); - /// tokio::runtime::Runtime::new() - /// .unwrap() - /// .block_on(async move { - /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - /// axum::Server::bind(&addr) - /// .serve( - /// axum::Router::new() - /// // Server side render the application, serve static assets, and register server functions - /// .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) - /// .into_make_service(), - /// ) - /// .await - /// .unwrap(); - /// }); + /// #[tokio::main] + /// async fn main() { + /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + /// axum::Server::bind(&addr) + /// .serve( + /// axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + /// .into_make_service(), + /// ) + /// .await + /// .unwrap(); /// } /// /// # fn app(cx: Scope) -> Element {todo!()} @@ -235,7 +221,7 @@ where fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { move |headers: HeaderMap, body: Request| async move { - server_fn_handler(().into(), func.clone(), headers, body).await + server_fn_handler((), func.clone(), headers, body).await } }) } @@ -314,11 +300,12 @@ async fn render_handler( /// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub async fn server_fn_handler( - server_context: DioxusServerContext, + server_context: impl Into, function: Arc, headers: HeaderMap, req: Request, ) -> impl IntoResponse { + let server_context = server_context.into(); let (_, body) = req.into_parts(); let body = hyper::body::to_bytes(body).await; let Ok(body)=body else { diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 357e1c19c..6bc8947a6 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -1,3 +1,54 @@ +//! Dioxus utilities for the [Salvo](https://salvo.rs) server framework. +//! +//! # Example +//! ```rust +//! # #![allow(non_snake_case)] +//! # use dioxus::prelude::*; +//! # use dioxus_server::prelude::*; +//! +//! fn main() { +//! #[cfg(feature = "web")] +//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); +//! #[cfg(feature = "ssr")] +//! { +//! use salvo::prelude::*; +//! GetServerData::register().unwrap(); +//! tokio::runtime::Runtime::new() +//! .unwrap() +//! .block_on(async move { +//! let router = +//! Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ())); +//! Server::new(TcpListener::bind("127.0.0.1:8080")) +//! .serve(router) +//! .await; +//! }); +//! } +//! } +//! +//! fn app(cx: Scope) -> Element { +//! let text = use_state(cx, || "...".to_string()); +//! +//! cx.render(rsx! { +//! button { +//! onclick: move |_| { +//! to_owned![text]; +//! async move { +//! if let Ok(data) = get_server_data().await { +//! text.set(data); +//! } +//! } +//! }, +//! "Run a server function" +//! } +//! "Server said: {text}" +//! }) +//! } +//! +//! #[server(GetServerData)] +//! async fn get_server_data() -> Result { +//! Ok("Hello from the server!".to_string()) +//! } + use std::{error::Error, sync::Arc}; use hyper::{http::HeaderValue, StatusCode}; @@ -11,14 +62,51 @@ use tokio::task::spawn_blocking; use crate::{ prelude::DioxusServerContext, - prelude::SSRState, - serve::ServeConfig, + render::SSRState, + serve_config::ServeConfig, server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; +/// A extension trait with utilities for integrating Dioxus with your Salvo router. pub trait DioxusRouterExt { - fn register_server_fns(self, server_fn_route: &'static str) -> Self; - + /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. + /// + /// ```rust + /// use salvo::prelude::*; + /// use std::{net::TcpListener, sync::Arc}; + /// use dioxus_server::prelude::*; + /// + /// struct ServerFunctionHandler { + /// server_fn: Arc, + /// } + /// + /// #[handler] + /// impl ServerFunctionHandler { + /// async fn handle( + /// &self, + /// req: &mut Request, + /// depot: &mut Depot, + /// res: &mut Response, + /// flow: &mut FlowCtrl, + /// ) { + /// // Add the headers to server context + /// ServerFnHandler::new((req.headers().clone(),), self.server_fn.clone()) + /// .handle(req, depot, res, flow) + /// .await + /// } + /// } + /// + /// #[tokio::main] + /// async fn main() { + /// let router = Router::new() + /// .register_server_fns_with_handler("", |func| { + /// ServerFnHandler::new(DioxusServerContext::default(), func) + /// }); + /// Server::new(TcpListener::bind("127.0.0.1:8080")) + /// .serve(router) + /// .await; + /// } + /// ``` fn register_server_fns_with_handler( self, server_fn_route: &'static str, @@ -27,13 +115,70 @@ pub trait DioxusRouterExt { where H: Handler + 'static; + /// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions. + /// + /// # Example + /// ```rust + /// use salvo::prelude::*; + /// use std::{net::TcpListener, sync::Arc}; + /// use dioxus_server::prelude::*; + /// + /// #[tokio::main] + /// async fn main() { + /// let router = Router::new() + /// .register_server_fns(""); + /// Server::new(TcpListener::bind("127.0.0.1:8080")) + /// .serve(router) + /// .await; + /// } + /// + /// ``` + fn register_server_fns(self, server_fn_route: &'static str) -> Self; + + /// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`]. + /// + /// # Example + /// ```rust + /// use salvo::prelude::*; + /// use std::{net::TcpListener, sync::Arc}; + /// use dioxus_server::prelude::*; + /// + /// #[tokio::main] + /// async fn main() { + /// let router = Router::new() + /// .connect_hot_reload(); + /// Server::new(TcpListener::bind("127.0.0.1:8080")) + /// .serve(router) + /// .await; + /// } + fn connect_hot_reload(self) -> Self; + + /// Serves the Dioxus application. This will serve a complete server side rendered application. + /// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading. + /// + /// # Example + /// ```rust + /// #![allow(non_snake_case)] + /// use dioxus::prelude::*; + /// use dioxus_server::prelude::*; + /// use salvo::prelude::*; + /// use std::{net::TcpListener, sync::Arc}; + /// + /// #[tokio::main] + /// async fn main() { + /// let router = Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ())); + /// Server::new(TcpListener::bind("127.0.0.1:8080")) + /// .serve(router) + /// .await; + /// } + /// + /// # fn app(cx: Scope) -> Element {todo!()} + /// ``` fn serve_dioxus_application( self, server_fn_path: &'static str, cfg: impl Into>, ) -> Self; - - fn connect_hot_reload(self) -> Self; } impl DioxusRouterExt for Router { @@ -138,13 +283,19 @@ impl Handler for SSRHandler

{ } } +/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub struct ServerFnHandler { server_context: DioxusServerContext, function: Arc, } impl ServerFnHandler { - pub fn new(server_context: DioxusServerContext, function: Arc) -> Self { + /// Create a new server function handler with the given server context and server function. + pub fn new( + server_context: impl Into, + function: Arc, + ) -> Self { + let server_context = server_context.into(); Self { server_context, function, @@ -234,6 +385,7 @@ fn handle_error(error: impl Error + Send + Sync, res: &mut Response) { *res = resp_err; } +/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change. #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] #[derive(Default)] pub struct HotReloadHandler; @@ -251,6 +403,7 @@ impl HotReloadHandler { } } +/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] #[derive(Default)] pub struct HotReloadHandler { From b8ca41bd92019b293d902195759e712a121b91cd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 12:15:59 -0500 Subject: [PATCH 33/87] document warp adapter --- packages/server/src/adapters/axum_adapter.rs | 30 ++-- packages/server/src/adapters/salvo_adapter.rs | 10 +- packages/server/src/adapters/warp_adapter.rs | 131 +++++++++++++++++- 3 files changed, 145 insertions(+), 26 deletions(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 34e6a5e46..c9bf9ba77 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -2,9 +2,9 @@ //! //! # Example //! ```rust -//! # #![allow(non_snake_case)] -//! # use dioxus::prelude::*; -//! # use dioxus_server::prelude::*; +//! #![allow(non_snake_case)] +//! use dioxus::prelude::*; +//! use dioxus_server::prelude::*; //! //! fn main() { //! #[cfg(feature = "web")] @@ -82,8 +82,8 @@ pub trait DioxusRouterExt { /// /// # Example /// ```rust - /// # use dioxus::prelude::*; - /// # use dioxus_server::prelude::*; + /// use dioxus::prelude::*; + /// use dioxus_server::prelude::*; /// /// #[tokio::main] /// async fn main() { @@ -117,8 +117,8 @@ pub trait DioxusRouterExt { /// /// # Example /// ```rust - /// # use dioxus::prelude::*; - /// # use dioxus_server::prelude::*; + /// use dioxus::prelude::*; + /// use dioxus_server::prelude::*; /// /// #[tokio::main] /// async fn main() { @@ -138,11 +138,9 @@ pub trait DioxusRouterExt { /// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`]. /// /// # Example - /// /// # Example /// ```rust - /// # #![allow(non_snake_case)] - /// # use dioxus::prelude::*; - /// # use dioxus_server::prelude::*; + /// #![allow(non_snake_case)] + /// use dioxus_server::prelude::*; /// /// #[tokio::main] /// async fn main() { @@ -158,8 +156,6 @@ pub trait DioxusRouterExt { /// .await /// .unwrap(); /// } - /// - /// # fn app(cx: Scope) -> Element {todo!()} /// ``` fn connect_hot_reload(self) -> Self; @@ -168,9 +164,9 @@ pub trait DioxusRouterExt { /// /// # Example /// ```rust - /// # #![allow(non_snake_case)] - /// # use dioxus::prelude::*; - /// # use dioxus_server::prelude::*; + /// #![allow(non_snake_case)] + /// use dioxus::prelude::*; + /// use dioxus_server::prelude::*; /// /// #[tokio::main] /// async fn main() { @@ -186,7 +182,7 @@ pub trait DioxusRouterExt { /// .unwrap(); /// } /// - /// # fn app(cx: Scope) -> Element {todo!()} + /// fn app(cx: Scope) -> Element {todo!()} /// ``` fn serve_dioxus_application( self, diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 6bc8947a6..ffe72bef5 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -2,9 +2,9 @@ //! //! # Example //! ```rust -//! # #![allow(non_snake_case)] -//! # use dioxus::prelude::*; -//! # use dioxus_server::prelude::*; +//! #![allow(non_snake_case)] +//! use dioxus::prelude::*; +//! use dioxus_server::prelude::*; //! //! fn main() { //! #[cfg(feature = "web")] @@ -48,6 +48,7 @@ //! async fn get_server_data() -> Result { //! Ok("Hello from the server!".to_string()) //! } +//! ``` use std::{error::Error, sync::Arc}; @@ -71,6 +72,7 @@ use crate::{ pub trait DioxusRouterExt { /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. /// + /// # Example /// ```rust /// use salvo::prelude::*; /// use std::{net::TcpListener, sync::Arc}; @@ -172,7 +174,7 @@ pub trait DioxusRouterExt { /// .await; /// } /// - /// # fn app(cx: Scope) -> Element {todo!()} + /// fn app(cx: Scope) -> Element {todo!()} /// ``` fn serve_dioxus_application( self, diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 67c3c75e4..5041e294d 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -1,3 +1,52 @@ +//! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework. +//! +//! # Example +//! ```rust +//! #![allow(non_snake_case)] +//! use dioxus::prelude::*; +//! use dioxus_server::prelude::*; +//! +//! fn main() { +//! #[cfg(feature = "web")] +//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); +//! #[cfg(feature = "ssr")] +//! { +//! GetServerData::register().unwrap(); +//! tokio::runtime::Runtime::new() +//! .unwrap() +//! .block_on(async move { +//! let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ())); +//! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +//! }); +//! } +//! } +//! +//! fn app(cx: Scope) -> Element { +//! let text = use_state(cx, || "...".to_string()); +//! +//! cx.render(rsx! { +//! button { +//! onclick: move |_| { +//! to_owned![text]; +//! async move { +//! if let Ok(data) = get_server_data().await { +//! text.set(data); +//! } +//! } +//! }, +//! "Run a server function" +//! } +//! "Server said: {text}" +//! }) +//! } +//! +//! #[server(GetServerData)] +//! async fn get_server_data() -> Result { +//! Ok("Hello from the server!".to_string()) +//! } +//! +//! ``` + use std::{error::Error, sync::Arc}; use server_fn::{Payload, ServerFunctionRegistry}; @@ -10,11 +59,36 @@ use warp::{ }; use crate::{ - prelude::{DioxusServerContext, SSRState}, - serve::ServeConfig, + prelude::DioxusServerContext, + render::SSRState, + serve_config::ServeConfig, server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, }; +/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. +/// +/// # Example +/// ```rust +/// use warp::{body, header, hyper::HeaderMap, path, post, Filter}; +/// +/// #[tokio::main] +/// async fn main() { +/// let routes = register_server_fns_with_handler("", |full_route, func| { +/// path(full_route) +/// .and(post()) +/// .and(header::headers_cloned()) +/// .and(body::bytes()) +/// .and_then(move |headers: HeaderMap, body| { +/// let func = func.clone(); +/// async move { +/// // Add the headers to the server function context +/// server_fn_handler((headers.clone(),), func, headers, body).await +/// } +/// }) +/// }); +/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +/// } +/// ``` pub fn register_server_fns_with_handler( server_fn_route: &'static str, mut handler: H, @@ -41,6 +115,18 @@ where filter.expect("No server functions found") } +/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions. +/// +/// # Example +/// ```rust +/// use dioxus_server::prelude::*; +/// +/// #[tokio::main] +/// async fn main() { +/// let routes = register_server_fns(""); +/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +/// } +/// ``` pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { register_server_fns_with_handler(server_fn_route, |full_route, func| { path(full_route) @@ -56,6 +142,25 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R }) } +/// Serves the Dioxus application. This will serve a complete server side rendered application. +/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading. +/// +/// # Example +/// ```rust +/// #![allow(non_snake_case)] +/// use dioxus::prelude::*; +/// use dioxus_server::prelude::*; +/// +/// #[tokio::main] +/// async fn main() { +/// let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ())); +/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +/// } +/// +/// fn app(cx: Scope) -> Element { +/// todo!() +/// } +/// ``` pub fn serve_dioxus_application( server_fn_route: &'static str, cfg: impl Into>, @@ -90,12 +195,14 @@ struct RecieveFailed(String); impl warp::reject::Reject for RecieveFailed {} -async fn server_fn_handler( - server_context: DioxusServerContext, +/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. +pub async fn server_fn_handler( + server_context: impl Into, function: Arc, headers: HeaderMap, body: Bytes, ) -> Result, warp::Rejection> { + let server_context = server_context.into(); // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); spawn_blocking({ @@ -161,7 +268,21 @@ fn report_err(e: E) -> Box { ) as Box } -pub fn connect_hot_reload() -> impl Filter { +/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`]. +/// +/// # Example +/// ```rust +/// #![allow(non_snake_case)] +/// use dioxus_server::prelude::*; +/// +/// #[tokio::main] +/// async fn main() { +/// let routes = connect_hot_reload(); +/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +/// } +/// ``` +pub fn connect_hot_reload() -> impl Filter + Clone +{ #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] { warp::path("_dioxus/hot_reload").and(warp::ws()).map(|| { From 10a1c46a49458db192410118394ee74e741c73e0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 12:45:01 -0500 Subject: [PATCH 34/87] make server crate example more compelling --- packages/server/src/lib.rs | 40 +++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 9654adebe..7ef232c23 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -6,6 +6,7 @@ //! - Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload). //! //! # Example +//! Full stack Dioxus in under 50 lines of code //! ```rust //! #![allow(non_snake_case)] //! use dioxus::prelude::*; @@ -13,50 +14,45 @@ //! //! fn main() { //! #[cfg(feature = "web")] -//! // Hydrate the application on the client //! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); //! #[cfg(feature = "ssr")] //! { -//! GetServerData::register().unwrap(); +//! GetMeaning::register().unwrap(); //! tokio::runtime::Runtime::new() //! .unwrap() //! .block_on(async move { -//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); -//! axum::Server::bind(&addr) -//! .serve( -//! axum::Router::new() -//! // Server side render the application, serve static assets, and register server functions -//! .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) -//! .into_make_service(), -//! ) -//! .await -//! .unwrap(); +//! warp::serve(serve_dioxus_application( +//! "", +//! ServeConfigBuilder::new(app, ()), +//! )) +//! .run(([127, 0, 0, 1], 8080)) +//! .await; //! }); -//! } +//! } //! } //! //! fn app(cx: Scope) -> Element { -//! let text = use_state(cx, || "...".to_string()); -//! +//! let meaning = use_state(cx, || None); //! cx.render(rsx! { //! button { //! onclick: move |_| { -//! to_owned![text]; +//! to_owned![meaning]; //! async move { -//! if let Ok(data) = get_server_data().await { -//! text.set(data.clone()); +//! if let Ok(data) = get_meaning("life the universe and everything".into()).await { +//! meaning.set(data); //! } //! } //! }, //! "Run a server function" //! } -//! "Server said: {text}" +//! "Server said: {meaning:?}" //! }) //! } //! -//! #[server(GetServerData)] -//! async fn get_server_data() -> Result { -//! Ok("Hello from the server!".to_string()) +//! // This code will only run on the server +//! #[server(GetMeaning)] +//! async fn get_meaning(of: String) -> Result, ServerFnError> { +//! Ok(of.contains("life").then(|| 42)) //! } //! ``` From 830dd6fa9737f6ead349093f3a492828ab341a4f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 13:09:25 -0500 Subject: [PATCH 35/87] use git version of server functions --- packages/server/Cargo.toml | 2 +- packages/server/server-macro/Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 563912754..fddeccbde 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] # server functions -server_fn = { path = "D:/Users/Desktop/github/leptos/server_fn", features = ["stable"] } +server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "a9e6590b5e7f1c0b01da7db7b86719cb18a4aaa1", features = ["stable"] } server_macro = { path = "server-macro" } # warp diff --git a/packages/server/server-macro/Cargo.toml b/packages/server/server-macro/Cargo.toml index e5fb6523e..d03c5274c 100644 --- a/packages/server/server-macro/Cargo.toml +++ b/packages/server/server-macro/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" [dependencies] quote = "1.0.26" -# server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "1e037ecb60965c7c55fd781fdc8de7863ffd102b", features = ["stable"] } -server_fn_macro = { path = "D:/Users/Desktop/github/leptos/server_fn_macro", features = ["stable"] } +server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "a9e6590b5e7f1c0b01da7db7b86719cb18a4aaa1", features = ["stable"] } syn = { version = "1", features = ["full"] } [lib] From f1c31d6cc368607c1310452b2554b4e06c936e2d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 13:09:34 -0500 Subject: [PATCH 36/87] mark examples as don't publish --- packages/server/examples/axum-hello-world/Cargo.toml | 1 + packages/server/examples/axum-router/Cargo.toml | 1 + packages/server/examples/salvo-hello-world/Cargo.toml | 1 + packages/server/examples/warp-hello-world/Cargo.toml | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index ed466e6e1..50d5e05b3 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -2,6 +2,7 @@ name = "axum-hello-world" version = "0.1.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml index 297aa6726..699779763 100644 --- a/packages/server/examples/axum-router/Cargo.toml +++ b/packages/server/examples/axum-router/Cargo.toml @@ -2,6 +2,7 @@ name = "axum-router" version = "0.1.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 4633eaafc..1ddf52915 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -2,6 +2,7 @@ name = "salvo-hello-world" version = "0.1.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index 86ac2da5b..cbd8606dd 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -2,6 +2,7 @@ name = "warp-hello-world" version = "0.1.0" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 7ae45272d1e7ee642501d574ecb32f9a7b6d9345 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 13:20:38 -0500 Subject: [PATCH 37/87] fix some doc links in dioxus-server --- packages/server/server-macro/src/lib.rs | 6 +++--- packages/server/src/adapters/axum_adapter.rs | 4 +++- packages/server/src/adapters/mod.rs | 4 ++-- packages/server/src/lib.rs | 6 ++++-- packages/server/src/server_fn.rs | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/server/server-macro/src/lib.rs b/packages/server/server-macro/src/lib.rs index f6b4f92a2..e34a440d4 100644 --- a/packages/server/server-macro/src/lib.rs +++ b/packages/server/server-macro/src/lib.rs @@ -19,7 +19,7 @@ use server_fn_macro::*; /// work without WebAssembly, the encoding must be `"Url"`. /// /// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext), +/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html), /// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other /// server-side context into the server function. /// @@ -50,8 +50,8 @@ use server_fn_macro::*; /// They are serialized as an `application/x-www-form-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 [DioxusServerContext](dioxus_server::prelude::DioxusServerContext) comes from the server.** Optionally, the first argument of a server function -/// can be a [DioxusServerContext](dioxus_server::prelude::DioxusServerContext). This scope can be used to inject dependencies like the HTTP request +/// - **The [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function +/// can be a [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html). 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. #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index c9bf9ba77..f4bdd8a96 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -182,7 +182,9 @@ pub trait DioxusRouterExt { /// .unwrap(); /// } /// - /// fn app(cx: Scope) -> Element {todo!()} + /// fn app(cx: Scope) -> Element { + /// todo!() + /// } /// ``` fn serve_dioxus_application( self, diff --git a/packages/server/src/adapters/mod.rs b/packages/server/src/adapters/mod.rs index 148062db0..5809132b8 100644 --- a/packages/server/src/adapters/mod.rs +++ b/packages/server/src/adapters/mod.rs @@ -6,8 +6,8 @@ //! Each framework has utilies for some or all of the following: //! - Server functions //! - A generic way to register server functions -//! - A way to register server functions with a custom handler that allows users to pass in a custom [`DioxusServerContext`] based on the state of the server framework. -//! - A way to register static WASM files that is accepts [`ServeConfig`] +//! - A way to register server functions with a custom handler that allows users to pass in a custom [`crate::server_context::DioxusServerContext`] based on the state of the server framework. +//! - A way to register static WASM files that is accepts [`crate::serve_config::ServeConfig`] //! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload) #[cfg(feature = "axum")] diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 7ef232c23..4738ef00f 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,8 +1,8 @@ //! Fullstack utilities for the [`dioxus`](https://dioxuslabs.com) framework. //! //! # Features -//! - Intigrations with the [axum](crate::adapters::axum_adapter), [salvo](crate::adapters::salvo_adapters), and [warp](crate::adapters::warp_adapters) server frameworks with utilities for serving and rendering Dioxus applications. -//! - Server functions that allow you to call code on the server from the client as if it were a normal function. +//! - Intigrations with the [axum](crate::adapters::axum_adapter), [salvo](crate::adapters::salvo_adapter), and [warp](crate::adapters::warp_adapter) server frameworks with utilities for serving and rendering Dioxus applications. +//! - [Server functions](crate::prelude::server) that allow you to call code on the server from the client as if it were a normal function. //! - Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload). //! //! # Example @@ -60,6 +60,8 @@ #[allow(unused)] use dioxus_core::prelude::*; +pub use adapters::*; + mod adapters; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 6f985a39a..643f25c9f 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -82,7 +82,7 @@ pub enum ServerRegistrationFnError { /// Server functions are created using the `server` macro. /// /// The function should be registered by calling `ServerFn::register()`. The set of server functions -/// can be queried on the server for routing purposes by calling [ServerFunctionRegistry::get]. +/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get]. /// /// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself. pub trait ServerFn: server_fn::ServerFn { From 0e445c9f5958eb5b2e64cf9fbe22e3b56978b3df Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 13:34:34 -0500 Subject: [PATCH 38/87] add desktop server functions example --- Cargo.toml | 3 +- .../server/examples/axum-desktop/.gitignore | 2 + .../server/examples/axum-desktop/Cargo.toml | 31 ++++++++++++++ .../examples/axum-desktop/src/client.rs | 13 ++++++ .../server/examples/axum-desktop/src/lib.rs | 40 +++++++++++++++++++ .../examples/axum-desktop/src/server.rs | 23 +++++++++++ .../examples/axum-hello-world/Cargo.toml | 3 +- 7 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 packages/server/examples/axum-desktop/.gitignore create mode 100644 packages/server/examples/axum-desktop/Cargo.toml create mode 100644 packages/server/examples/axum-desktop/src/client.rs create mode 100644 packages/server/examples/axum-desktop/src/lib.rs create mode 100644 packages/server/examples/axum-desktop/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 720b9e274..a5beb4aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,10 @@ members = [ "packages/server", "packages/server/server-macro", "packages/server/examples/axum-hello-world", + "packages/server/examples/axum-router", + "packages/server/examples/axum-desktop", "packages/server/examples/salvo-hello-world", "packages/server/examples/warp-hello-world", - "packages/server/examples/axum-router", "docs/guide", ] diff --git a/packages/server/examples/axum-desktop/.gitignore b/packages/server/examples/axum-desktop/.gitignore new file mode 100644 index 000000000..6047329c6 --- /dev/null +++ b/packages/server/examples/axum-desktop/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/packages/server/examples/axum-desktop/Cargo.toml b/packages/server/examples/axum-desktop/Cargo.toml new file mode 100644 index 000000000..817202bd1 --- /dev/null +++ b/packages/server/examples/axum-desktop/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "axum-desktop" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] + +[dependencies] +dioxus-desktop = { path = "../../../desktop", optional = true } +dioxus = { path = "../../../dioxus" } +dioxus-router = { path = "../../../router" } +dioxus-server = { path = "../../" } +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } +serde = "1.0.159" + +[features] +default = [] +ssr = ["axum", "tokio", "dioxus-server/axum"] +desktop = ["dioxus-desktop"] + +[[bin]] +name = "client" +path = "src/client.rs" +required-features = ["desktop"] + +[[bin]] +name = "server" +path = "src/server.rs" +required-features = ["ssr"] diff --git a/packages/server/examples/axum-desktop/src/client.rs b/packages/server/examples/axum-desktop/src/client.rs new file mode 100644 index 000000000..588cf2003 --- /dev/null +++ b/packages/server/examples/axum-desktop/src/client.rs @@ -0,0 +1,13 @@ +// Run with: +// ```bash +// cargo run --bin client --features="desktop" +// ``` + +use axum_desktop::*; +use dioxus_server::prelude::server_fn::set_server_url; + +fn main() { + // Set the url of the server where server functions are hosted. + set_server_url("http://localhost:8080"); + dioxus_desktop::launch(app) +} diff --git a/packages/server/examples/axum-desktop/src/lib.rs b/packages/server/examples/axum-desktop/src/lib.rs new file mode 100644 index 000000000..3bae1dcde --- /dev/null +++ b/packages/server/examples/axum-desktop/src/lib.rs @@ -0,0 +1,40 @@ +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +pub fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + let text = use_state(cx, || "...".to_string()); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![text]; + async move { + if let Ok(data) = get_server_data().await { + println!("Client received: {}", data); + text.set(data.clone()); + post_server_data(data).await.unwrap(); + } + } + }, + "Run a server function" + } + "Server said: {text}" + }) +} + +#[server(PostServerData)] +async fn post_server_data(data: String) -> Result<(), ServerFnError> { + println!("Server received: {}", data); + + Ok(()) +} + +#[server(GetServerData)] +async fn get_server_data() -> Result { + Ok("Hello from the server!".to_string()) +} diff --git a/packages/server/examples/axum-desktop/src/server.rs b/packages/server/examples/axum-desktop/src/server.rs new file mode 100644 index 000000000..ff607dbcd --- /dev/null +++ b/packages/server/examples/axum-desktop/src/server.rs @@ -0,0 +1,23 @@ +// Run with: +// ```bash +// cargo run --bin server --features="ssr" +// ``` + +use axum_desktop::*; +use dioxus_server::prelude::*; + +#[tokio::main] +async fn main() { + PostServerData::register().unwrap(); + GetServerData::register().unwrap(); + + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .register_server_fns("") + .into_make_service(), + ) + .await + .unwrap(); +} diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index 50d5e05b3..fd1f051d1 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -9,7 +9,6 @@ publish = false [dependencies] dioxus-web = { path = "../../../web", features=["hydrate"], optional = true } dioxus = { path = "../../../dioxus" } -dioxus-router = { path = "../../../router" } dioxus-server = { path = "../../" } axum = { version = "0.6.12", optional = true } tokio = { version = "1.27.0", features = ["full"], optional = true } @@ -18,4 +17,4 @@ serde = "1.0.159" [features] default = ["web"] ssr = ["axum", "tokio", "dioxus-server/axum"] -web = ["dioxus-web", "dioxus-router/web"] +web = ["dioxus-web"] From 53c85851076462cd002a6c85135cca5f7bdbe1b0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 13:58:54 -0500 Subject: [PATCH 39/87] Add dixous-server README --- packages/server/Cargo.toml | 6 ++ packages/server/README.md | 103 ++++++++++++++++++++++++ packages/server/server-macro/src/lib.rs | 6 +- packages/server/src/lib.rs | 65 +-------------- 4 files changed, 116 insertions(+), 64 deletions(-) create mode 100644 packages/server/README.md diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index fddeccbde..8709cb57d 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -2,6 +2,12 @@ name = "dioxus-server" version = "0.1.0" edition = "2021" +description = "Fullstack Dioxus Utilities" +license = "MIT/Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 000000000..92f878684 --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1,103 @@ +# Dioxus Server + +[![Crates.io][crates-badge]][crates-url] +[![MIT licensed][mit-badge]][mit-url] +[![Build Status][actions-badge]][actions-url] +[![Discord chat][discord-badge]][discord-url] + +[crates-badge]: https://img.shields.io/crates/v/dioxus-server.svg +[crates-url]: https://crates.io/crates/dioxus-server +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE +[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg +[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster +[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square +[discord-url]: https://discord.gg/XgGxMSkvUM + +[Website](https://dioxuslabs.com) | +[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) | +[API Docs](https://docs.rs/dioxus-server/latest/dioxus_sever) | +[Chat](https://discord.gg/XgGxMSkvUM) + +Fullstack utilities for the [`Dioxus`](https://dioxuslabs.com) framework. + +# Features + +- Intigrations with the [Axum](https::/docs.rs/dioxus-server/latest/dixous_server/axum_adapter/index.html), [Salvo](https::/docs.rs/dioxus-server/latest/dixous_server/salvo_adapter/index.html), and [Warp](https::/docs.rs/dioxus-server/latest/dixous_server/warp_adapter/index.html) server frameworks with utilities for serving and rendering Dioxus applications. +- [Server functions](https::/docs.rs/dioxus-server/latest/dixous_server/prelude/attr.server.html) allow you to call code on the server from the client as if it were a normal function. +- Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload). + +# Example + +Full stack Dioxus in under 50 lines of code + +```rust +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + GetMeaning::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + warp::serve( + // Automatically handles server side rendering, hot reloading intigration, and hosting server functions + serve_dioxus_application( + "", + ServeConfigBuilder::new(app, ()), + ) + ) + .run(([127, 0, 0, 1], 8080)) + .await; + }); + } +} + +fn app(cx: Scope) -> Element { + let meaning = use_state(cx, || None); + cx.render(rsx! { + button { + onclick: move |_| { + to_owned![meaning]; + async move { + if let Ok(data) = get_meaning("life the universe and everything".into()).await { + meaning.set(data); + } + } + }, + "Run a server function" + } + "Server said: {meaning:?}" + }) +} + +// This code will only run on the server +#[server(GetMeaning)] +async fn get_meaning(of: String) -> Result, ServerFnError> { + Ok(of.contains("life").then(|| 42)) +} +``` + +## Getting Started + +To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/docs/nightly/guide/en/getting_started/ssr.html), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/packages/server/examples). + +## Contributing + +- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). +- Join the discord and ask questions! + +## License + +This project is licensed under the [MIT license]. + +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you shall be licensed as MIT without any additional +terms or conditions. diff --git a/packages/server/server-macro/src/lib.rs b/packages/server/server-macro/src/lib.rs index e34a440d4..14fc0ac77 100644 --- a/packages/server/server-macro/src/lib.rs +++ b/packages/server/server-macro/src/lib.rs @@ -19,7 +19,7 @@ use server_fn_macro::*; /// work without WebAssembly, the encoding must be `"Url"`. /// /// The server function itself can take any number of arguments, each of which should be serializable -/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html), +/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-server/latest/dixous_server/prelude/struct.DioxusServerContext.html), /// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other /// server-side context into the server function. /// @@ -50,8 +50,8 @@ use server_fn_macro::*; /// They are serialized as an `application/x-www-form-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 [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function -/// can be a [DioxusServerContext](https::/docs.rs/dioxus_server/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request +/// - **The [DioxusServerContext](https::/docs.rs/dioxus-server/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function +/// can be a [DioxusServerContext](https::/docs.rs/dioxus-server/latest/dixous_server/prelude/struct.DioxusServerContext.html). 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. #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 4738ef00f..c61b26429 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -1,64 +1,7 @@ -//! Fullstack utilities for the [`dioxus`](https://dioxuslabs.com) framework. -//! -//! # Features -//! - Intigrations with the [axum](crate::adapters::axum_adapter), [salvo](crate::adapters::salvo_adapter), and [warp](crate::adapters::warp_adapter) server frameworks with utilities for serving and rendering Dioxus applications. -//! - [Server functions](crate::prelude::server) that allow you to call code on the server from the client as if it were a normal function. -//! - Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload). -//! -//! # Example -//! Full stack Dioxus in under 50 lines of code -//! ```rust -//! #![allow(non_snake_case)] -//! use dioxus::prelude::*; -//! use dioxus_server::prelude::*; -//! -//! fn main() { -//! #[cfg(feature = "web")] -//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); -//! #[cfg(feature = "ssr")] -//! { -//! GetMeaning::register().unwrap(); -//! tokio::runtime::Runtime::new() -//! .unwrap() -//! .block_on(async move { -//! warp::serve(serve_dioxus_application( -//! "", -//! ServeConfigBuilder::new(app, ()), -//! )) -//! .run(([127, 0, 0, 1], 8080)) -//! .await; -//! }); -//! } -//! } -//! -//! fn app(cx: Scope) -> Element { -//! let meaning = use_state(cx, || None); -//! cx.render(rsx! { -//! button { -//! onclick: move |_| { -//! to_owned![meaning]; -//! async move { -//! if let Ok(data) = get_meaning("life the universe and everything".into()).await { -//! meaning.set(data); -//! } -//! } -//! }, -//! "Run a server function" -//! } -//! "Server said: {meaning:?}" -//! }) -//! } -//! -//! // This code will only run on the server -//! #[server(GetMeaning)] -//! async fn get_meaning(of: String) -> Result, ServerFnError> { -//! Ok(of.contains("life").then(|| 42)) -//! } -//! ``` - -#![warn(missing_docs)] -#[allow(unused)] -use dioxus_core::prelude::*; +#![doc = include_str!("../README.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] +#![deny(missing_docs)] pub use adapters::*; From 4c22d5809e92014d19628db467ad7eb41345d3bd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 16:06:23 -0500 Subject: [PATCH 40/87] add fullstack section to the getting started guide --- docs/guide/Cargo.toml | 3 +- docs/guide/examples/server.rs | 132 ++++++++++++++++++ docs/guide/src/en/SUMMARY.md | 1 + .../guide/src/en/getting_started/fullstack.md | 115 +++++++++++++++ .../src/en/getting_started/hot_reload.md | 15 +- docs/guide/src/en/getting_started/ssr.md | 21 +-- docs/guide/src/en/getting_started/web.md | 6 +- 7 files changed, 273 insertions(+), 20 deletions(-) create mode 100644 docs/guide/examples/server.rs create mode 100644 docs/guide/src/en/getting_started/fullstack.md diff --git a/docs/guide/Cargo.toml b/docs/guide/Cargo.toml index 227bdfa35..00a5c7543 100644 --- a/docs/guide/Cargo.toml +++ b/docs/guide/Cargo.toml @@ -16,6 +16,7 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" } dioxus-router = { path = "../../packages/router" } dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] } dioxus-tui = { path = "../../packages/dioxus-tui" } +dioxus-server = { path = "../../packages/server" } fermi = { path = "../../packages/fermi" } shipyard = "0.6.2" @@ -23,5 +24,5 @@ shipyard = "0.6.2" # dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] } serde = { version = "1.0.138", features=["derive"] } reqwest = { version = "0.11.11", features = ["json"] } -tokio = { version = "1.19.2" , features=[]} +tokio = { version = "1.19.2", features = ["full"] } axum = { version = "0.6.1", features = ["ws"] } diff --git a/docs/guide/examples/server.rs b/docs/guide/examples/server.rs new file mode 100644 index 000000000..80e74a919 --- /dev/null +++ b/docs/guide/examples/server.rs @@ -0,0 +1,132 @@ +mod basic { + // ANCHOR: basic + #![allow(non_snake_case)] + use dioxus::prelude::*; + use dioxus_server::prelude::*; + + #[tokio::main] + async fn main() { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + } + + fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) + } + // ANCHOR_END: basic +} + +mod hydration { + // ANCHOR: hydration + #![allow(non_snake_case)] + use dioxus::prelude::*; + use dioxus_server::prelude::*; + + fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + }); + } + } + + fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) + } + // ANCHOR_END: hydration +} + +mod server_function { + // ANCHOR: server_function + #![allow(non_snake_case)] + use dioxus::prelude::*; + use dioxus_server::prelude::*; + + fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + // Register the server function before starting the server + DoubleServer::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + // Serve Dioxus application automatically recognizes server functions and adds them to the API + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + }); + } + } + + fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![count]; + async move { + // Call the server function just like a local async function + if let Ok(new_count) = double_server(*count.current()).await { + count.set(new_count); + } + } + }, + "Double" + } + }) + } + + #[server(DoubleServer)] + async fn double_server(number: u32) -> Result { + // Perform some expensive computation or access a database on the server + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let result = number * 2; + println!("server calculated {result}"); + Ok(result) + } + // ANCHOR_END: server_function +} diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index f9f31ed52..b61a36117 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -6,6 +6,7 @@ - [Desktop](getting_started/desktop.md) - [Web](getting_started/web.md) - [Server-Side Rendering](getting_started/ssr.md) + - [Fullstack](getting_started/fullstack.md) - [Liveview](getting_started/liveview.md) - [Terminal UI](getting_started/tui.md) - [Mobile](getting_started/mobile.md) diff --git a/docs/guide/src/en/getting_started/fullstack.md b/docs/guide/src/en/getting_started/fullstack.md new file mode 100644 index 000000000..b132de319 --- /dev/null +++ b/docs/guide/src/en/getting_started/fullstack.md @@ -0,0 +1,115 @@ +> This guide assumes you read the [Web](web.md) guide and installed the [Dioxus-cli](https://github.com/DioxusLabs/cli) + +# Fullstack development + +We can combine the `dioxus-web` renderer with the `dioxus-ssr` renderer to create a full-stack Dioxus application. By combining server-side rendering with client-side hydration we can create an application that is initially rendered on the server and then hydrates the application on the client. Server-side rendering results in a fast first paint and make our page SEO-friendly. Client-side hydration makes our page responsive once the application has fully loaded. + +To help make full-stack development easier, Dioxus provides a `dioxus-server` crate that integrates with popular web frameworks with utilities for full-stack development. + +## Setup + +For this guide, we're going to show how to use Dioxus with [Axum](https://docs.rs/axum/latest/axum/), but `dioxus-server` also integrates with the [Warp](https://docs.rs/warp/latest/warp/) and [Salvo](https://docs.rs/salvo/latest/salvo/) web frameworks. + +Make sure you have Rust and Cargo installed, and then create a new project: + +```shell +cargo new --bin demo +cd demo +``` + +Add `dioxus` and `dioxus-server` as dependencies: + +```shell +cargo add dioxus +cargo add dioxus-server --features axum, ssr +``` + +Next, add all the Axum dependencies. This will be different if you're using a different Web Framework + +```shell +cargo add tokio --features full +cargo add axum +``` + +Your dependencies should look roughly like this: + +```toml +[dependencies] +axum = "*" +dioxus = { version = "*" } +dioxus-server = { version = "*", features = ["axum", "ssr"] } +tokio = { version = "*", features = ["full"] } +``` + +Now, set up your Axum app to serve the Dioxus app. + +```rust +{{#include ../../../examples/server.rs:basic}} +``` + +Now, run your app with `cargo run` and open `http://localhost:8080` in your browser. You should see a server-side rendered page with a counter. + +## Hydration + +Right now, the page is static. We can't interact with the buttons. To fix this, we can hydrate the page with `dioxus-web`. + +First, modify your `Cargo.toml` to include two features, one for the server called `ssr`, and one for the client called `web`. + +```toml +[dependencies] +# Common dependancies +dioxus = { version = "*" } +dioxus-server = { version = "*" } + +# Web dependancies +dioxus-web = { version = "*", features=["hydrate"], optional = true } + +# Server dependancies +axum = { version = "0.6.12", optional = true } +tokio = { version = "1.27.0", features = ["full"], optional = true } + +[features] +default = [] +ssr = ["axum", "tokio", "dioxus-server/axum"] +web = ["dioxus-web"] +``` + +Next, we need to modify our `main.rs` to use either hydrate on the client or render on the server depending on the active features. + +```rust +{{#include ../../../examples/server.rs:hydration}} +``` + +Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons! + +## Communicating with the server + +`dixous-server` provides server functions that allow you to call an automatically generated API on the server from the client as if it were a local function. + +To make a server function, simply add the `#[server(YourUniqueType)]` attribute to a function. The function must: + +- Be an async function +- Have arguments and a return type that both implement serialize and deserialize (with [serde](https://serde.rs/)). +- Return a `Result` with an error type of ServerFnError + +You must call `register` on the type you passed into the server macro in your main function before starting your server to tell Dioxus about the server function. + +Let's add a server function to our app that allows us to multiply the count by a number on the server. + +First, add serde as a dependency: + +```shell +cargo add serde +``` + +Next, add the server function to your `main.rs`: + +```rust +{{#include ../../../examples/server.rs:server_function}} +``` + +Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2. + +## Conclusion + +That's it! You've created a full-stack Dioxus app. You can find more examples of full-stack apps and information about how to integrate with other frameworks and desktop renderers in the [dioxus-server examples directory](https://github.com/DioxusLabs/dioxus/tree/master/packages/server/examples). diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index 599ce57da..2ebf5e4aa 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -5,28 +5,35 @@ 3. Currently the cli only implements hot reloading for the web renderer. For TUI, desktop, and LiveView you can use the hot reload macro instead. # Web + For the web renderer, you can use the dioxus cli to serve your application with hot reloading enabled. ## Setup + Install [dioxus-cli](https://github.com/DioxusLabs/cli). Hot reloading is automatically enabled when using the web renderer on debug builds. ## Usage + 1. Run: -```bash + +```bash dioxus serve --hot-reload ``` + 2. Change some code within a rsx or render macro 3. Open your localhost in a browser 4. Save and watch the style change without recompiling -# Desktop/Liveview/TUI +# Desktop/Liveview/TUI/Server + For desktop, LiveView, and tui, you can place the hot reload macro at the top of your main function to enable hot reloading. Hot reloading is automatically enabled on debug builds. For more information about hot reloading on native platforms and configuration options see the [dioxus-hot-reload](https://crates.io/crates/dioxus-hot-reload) crate. ## Setup + Add the following to your main function: ```rust @@ -37,13 +44,17 @@ fn main() { ``` ## Usage + 1. Run: + ```bash cargo run ``` + 2. Change some code within a rsx or render macro 3. Save and watch the style change without recompiling # Limitations + 1. The interpreter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will require a full recompile to capture the expression. 2. Components, Iterators, and some attributes can contain arbitrary rust code and will trigger a full recompile when changed. diff --git a/docs/guide/src/en/getting_started/ssr.md b/docs/guide/src/en/getting_started/ssr.md index 9654003cf..f78623003 100644 --- a/docs/guide/src/en/getting_started/ssr.md +++ b/docs/guide/src/en/getting_started/ssr.md @@ -1,17 +1,6 @@ # Server-Side Rendering -The Dioxus VirtualDom can be rendered server-side. - -[Example: Dioxus DocSite](https://github.com/dioxusLabs/docsite) - -## Multithreaded Support - -The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. - -To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels. - -When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String – but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to create a pool of VirtualDoms. - +For lower-level control over the rendering process, you can use the `dioxus-ssr` crate directly. This can be useful when integrating with a web framework that `dioxus-server` does not support, or pre-rendering pages. ## Setup @@ -21,7 +10,7 @@ Make sure you have Rust and Cargo installed, and then create a new project: ```shell cargo new --bin demo -cd app +cd demo ``` Add Dioxus and the ssr renderer as dependencies: @@ -99,6 +88,8 @@ async fn app_endpoint() -> Html { } ``` -And that's it! +## Multithreaded Support -> You might notice that you cannot hold the VirtualDom across an await point. Dioxus is currently not ThreadSafe, so it _must_ remain on the thread it started. We are working on loosening this requirement. \ No newline at end of file +The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. +When working with web frameworks that require `Send`, it is possible to render a VirtualDom immediately to a String – but you cannot hold the VirtualDom across an await point. For retained-state SSR (essentially LiveView), you'll need to spawn a VirtualDom on its own thread and communicate with it via channels or create a pool of VirtualDoms. +You might notice that you cannot hold the VirtualDom across an await point. Because Dioxus is currently not ThreadSafe, it _must_ remain on the thread it started. We are working on loosening this requirement. diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index 76e3b9541..e8a1f3da2 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -5,6 +5,7 @@ Build single-page applications that run in the browser with Dioxus. To run on th A build of Dioxus for the web will be roughly equivalent to the size of a React build (70kb vs 65kb) but it will load significantly faster because [WebAssembly can be compiled as it is streamed](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/). Examples: + - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) - [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site) @@ -17,7 +18,7 @@ Examples: The Web is the best-supported target platform for Dioxus. - Because your app will be compiled to WASM you have access to browser APIs through [wasm-bingen](https://rustwasm.github.io/docs/wasm-bindgen/introduction.html). -- Dioxus provides hydration to resume apps that are rendered on the server. See the [hydration example](https://github.com/DioxusLabs/dioxus/blob/master/packages/web/examples/hydrate.rs) for more details. +- Dioxus provides hydration to resume apps that are rendered on the server. See the [fullstack](fullstack.md) getting started guide for more information. ## Tooling @@ -28,6 +29,7 @@ cargo install dioxus-cli ``` Make sure the `wasm32-unknown-unknown` target for rust is installed: + ```shell rustup target add wasm32-unknown-unknown ``` @@ -49,11 +51,11 @@ cargo add dioxus-web ``` Edit your `main.rs`: + ```rust {{#include ../../../examples/hello_world_web.rs}} ``` - And to serve our app: ```bash From 219af51526ffb758fbf99a4113099639bb56e398 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 3 Apr 2023 17:39:09 -0500 Subject: [PATCH 41/87] fix cargo check --- docs/guide/examples/hydration.rs | 34 +++++ docs/guide/examples/server.rs | 132 ------------------ docs/guide/examples/server_basic.rs | 30 ++++ docs/guide/examples/server_function.rs | 58 ++++++++ .../guide/src/en/getting_started/fullstack.md | 6 +- packages/router/src/components/router.rs | 7 +- packages/server/Cargo.toml | 2 +- packages/server/server-macro/Cargo.toml | 2 +- packages/server/src/lib.rs | 4 +- packages/server/src/render.rs | 3 +- 10 files changed, 136 insertions(+), 142 deletions(-) create mode 100644 docs/guide/examples/hydration.rs delete mode 100644 docs/guide/examples/server.rs create mode 100644 docs/guide/examples/server_basic.rs create mode 100644 docs/guide/examples/server_function.rs diff --git a/docs/guide/examples/hydration.rs b/docs/guide/examples/hydration.rs new file mode 100644 index 000000000..f3c9bca8f --- /dev/null +++ b/docs/guide/examples/hydration.rs @@ -0,0 +1,34 @@ +#![allow(non_snake_case, unused)] +use dioxus::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + use dioxus_server::prelude::*; + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) +} diff --git a/docs/guide/examples/server.rs b/docs/guide/examples/server.rs deleted file mode 100644 index 80e74a919..000000000 --- a/docs/guide/examples/server.rs +++ /dev/null @@ -1,132 +0,0 @@ -mod basic { - // ANCHOR: basic - #![allow(non_snake_case)] - use dioxus::prelude::*; - use dioxus_server::prelude::*; - - #[tokio::main] - async fn main() { - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - axum::Server::bind(&addr) - .serve( - axum::Router::new() - .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) - .into_make_service(), - ) - .await - .unwrap(); - } - - fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); - - cx.render(rsx! { - h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } - }) - } - // ANCHOR_END: basic -} - -mod hydration { - // ANCHOR: hydration - #![allow(non_snake_case)] - use dioxus::prelude::*; - use dioxus_server::prelude::*; - - fn main() { - #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); - #[cfg(feature = "ssr")] - { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - axum::Server::bind(&addr) - .serve( - axum::Router::new() - .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) - .into_make_service(), - ) - .await - .unwrap(); - }); - } - } - - fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); - - cx.render(rsx! { - h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } - }) - } - // ANCHOR_END: hydration -} - -mod server_function { - // ANCHOR: server_function - #![allow(non_snake_case)] - use dioxus::prelude::*; - use dioxus_server::prelude::*; - - fn main() { - #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); - #[cfg(feature = "ssr")] - { - // Register the server function before starting the server - DoubleServer::register().unwrap(); - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); - axum::Server::bind(&addr) - .serve( - axum::Router::new() - // Serve Dioxus application automatically recognizes server functions and adds them to the API - .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) - .into_make_service(), - ) - .await - .unwrap(); - }); - } - } - - fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); - - cx.render(rsx! { - h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } - button { - onclick: move |_| { - to_owned![count]; - async move { - // Call the server function just like a local async function - if let Ok(new_count) = double_server(*count.current()).await { - count.set(new_count); - } - } - }, - "Double" - } - }) - } - - #[server(DoubleServer)] - async fn double_server(number: u32) -> Result { - // Perform some expensive computation or access a database on the server - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let result = number * 2; - println!("server calculated {result}"); - Ok(result) - } - // ANCHOR_END: server_function -} diff --git a/docs/guide/examples/server_basic.rs b/docs/guide/examples/server_basic.rs new file mode 100644 index 000000000..bd6cf78e6 --- /dev/null +++ b/docs/guide/examples/server_basic.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case, unused)] +use dioxus::prelude::*; + +#[tokio::main] +async fn main() { + #[cfg(feature = "ssr")] + { + use dioxus_server::prelude::*; + + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) +} diff --git a/docs/guide/examples/server_function.rs b/docs/guide/examples/server_function.rs new file mode 100644 index 000000000..106c54c3f --- /dev/null +++ b/docs/guide/examples/server_function.rs @@ -0,0 +1,58 @@ +#![allow(non_snake_case, unused)] +use dioxus::prelude::*; +use dioxus_server::prelude::*; + +fn main() { + #[cfg(feature = "web")] + dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + #[cfg(feature = "ssr")] + { + // Register the server function before starting the server + DoubleServer::register().unwrap(); + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); + axum::Server::bind(&addr) + .serve( + axum::Router::new() + // Serve Dioxus application automatically recognizes server functions and adds them to the API + .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .into_make_service(), + ) + .await + .unwrap(); + }); + } +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render(rsx! { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + button { + onclick: move |_| { + to_owned![count]; + async move { + // Call the server function just like a local async function + if let Ok(new_count) = double_server(*count.current()).await { + count.set(new_count); + } + } + }, + "Double" + } + }) +} + +#[server(DoubleServer)] +async fn double_server(number: u32) -> Result { + // Perform some expensive computation or access a database on the server + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let result = number * 2; + println!("server calculated {result}"); + Ok(result) +} diff --git a/docs/guide/src/en/getting_started/fullstack.md b/docs/guide/src/en/getting_started/fullstack.md index b132de319..f01d8dd30 100644 --- a/docs/guide/src/en/getting_started/fullstack.md +++ b/docs/guide/src/en/getting_started/fullstack.md @@ -44,7 +44,7 @@ tokio = { version = "*", features = ["full"] } Now, set up your Axum app to serve the Dioxus app. ```rust -{{#include ../../../examples/server.rs:basic}} +{{#include ../../../examples/server_basic.rs}} ``` Now, run your app with `cargo run` and open `http://localhost:8080` in your browser. You should see a server-side rendered page with a counter. @@ -77,7 +77,7 @@ web = ["dioxus-web"] Next, we need to modify our `main.rs` to use either hydrate on the client or render on the server depending on the active features. ```rust -{{#include ../../../examples/server.rs:hydration}} +{{#include ../../../examples/hydration.rs}} ``` Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons! @@ -105,7 +105,7 @@ cargo add serde Next, add the server function to your `main.rs`: ```rust -{{#include ../../../examples/server.rs:server_function}} +{{#include ../../../examples/server_function.rs}} ``` Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2. diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index b027386c5..8d5365c35 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -29,8 +29,9 @@ pub struct RouterProps<'a> { pub active_class: Option<&'a str>, /// Set the initial url. - #[props(!optional, into)] - pub initial_url: Option, + // This is Option> because we want to be able to either omit the prop or pass in Option + #[props(into)] + pub initial_url: Option>, } /// A component that conditionally renders children based on the current location of the app. @@ -46,7 +47,7 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { RouterCfg { base_url: cx.props.base_url.map(|s| s.to_string()), active_class: cx.props.active_class.map(|s| s.to_string()), - initial_url: cx.props.initial_url.clone(), + initial_url: cx.props.initial_url.clone().flatten(), }, )) }); diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 8709cb57d..18633e97a 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"] [dependencies] # server functions server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "a9e6590b5e7f1c0b01da7db7b86719cb18a4aaa1", features = ["stable"] } -server_macro = { path = "server-macro" } +dioxus_server_macro = { path = "server-macro" } # warp warp = { version = "0.3.3", optional = true } diff --git a/packages/server/server-macro/Cargo.toml b/packages/server/server-macro/Cargo.toml index d03c5274c..45b0ec3c6 100644 --- a/packages/server/server-macro/Cargo.toml +++ b/packages/server/server-macro/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "server_macro" +name = "dioxus_server_macro" version = "0.1.0" edition = "2021" diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index c61b26429..df2735cf4 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -24,11 +24,13 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(feature = "ssr")] + pub use crate::render::SSRState; + #[cfg(feature = "ssr")] pub use crate::serve_config::{ServeConfig, ServeConfigBuilder}; pub use crate::server_context::DioxusServerContext; pub use crate::server_fn::ServerFn; #[cfg(feature = "ssr")] pub use crate::server_fn::ServerFnTraitObj; + pub use dioxus_server_macro::*; pub use server_fn::{self, ServerFn as _, ServerFnError}; - pub use server_macro::*; } diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 22e751ea8..8328be317 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -9,7 +9,7 @@ use crate::prelude::ServeConfig; /// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders. #[derive(Clone)] -pub(crate) struct SSRState { +pub struct SSRState { // We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move renderers: Arc>, } @@ -23,6 +23,7 @@ impl Default for SSRState { } impl SSRState { + /// Render the application to HTML. pub fn render(&self, cfg: &ServeConfig

) -> String { let ServeConfig { app, props, index, .. From 61dba2825cf9430c8ebd0e6bdaeaa6fe2c2a4d7d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 24 Apr 2023 11:16:53 -0500 Subject: [PATCH 42/87] Improve the custom hooks chapter --- docs/guide/Cargo.toml | 6 +- docs/guide/examples/hooks_anti_patterns.rs | 38 +++++++++++ docs/guide/examples/hooks_composed.rs | 54 +++++++++++++++ docs/guide/examples/hooks_custom_logic.rs | 57 ++++++++++++++++ .../src/en/interactivity/custom_hooks.md | 65 +++++++++++++++++++ 5 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 docs/guide/examples/hooks_anti_patterns.rs create mode 100644 docs/guide/examples/hooks_custom_logic.rs diff --git a/docs/guide/Cargo.toml b/docs/guide/Cargo.toml index 227bdfa35..3919dc11c 100644 --- a/docs/guide/Cargo.toml +++ b/docs/guide/Cargo.toml @@ -16,12 +16,12 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" } dioxus-router = { path = "../../packages/router" } dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] } dioxus-tui = { path = "../../packages/dioxus-tui" } -fermi = { path = "../../packages/fermi" } -shipyard = "0.6.2" - # dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] } +fermi = { path = "../../packages/fermi" } +shipyard = "0.6.2" serde = { version = "1.0.138", features=["derive"] } reqwest = { version = "0.11.11", features = ["json"] } tokio = { version = "1.19.2" , features=[]} axum = { version = "0.6.1", features = ["ws"] } +gloo-storage = "0.2.2" diff --git a/docs/guide/examples/hooks_anti_patterns.rs b/docs/guide/examples/hooks_anti_patterns.rs new file mode 100644 index 000000000..2119b8601 --- /dev/null +++ b/docs/guide/examples/hooks_anti_patterns.rs @@ -0,0 +1,38 @@ +#![allow(unused)] + +use dioxus::prelude::*; + +fn main() {} + +// ANCHOR: non_clone_state +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +struct UseState<'a, T> { + value: &'a RefCell, + update: Arc, +} + +fn my_use_state(cx: &ScopeState, init: impl FnOnce() -> T) -> UseState { + // The update function will trigger a re-render in the component cx is attached to + let update = cx.schedule_update(); + // Create the initial state + let value = cx.use_hook(|| RefCell::new(init())); + + UseState { value, update } +} + +impl UseState<'_, T> { + fn get(&self) -> T { + self.value.borrow().clone() + } + + fn set(&self, value: T) { + // Update the state + *self.value.borrow_mut() = value; + // Trigger a re-render on the component the state is from + (self.update)(); + } +} +// ANCHOR_END: non_clone_state diff --git a/docs/guide/examples/hooks_composed.rs b/docs/guide/examples/hooks_composed.rs index 801474aa3..782a19d7d 100644 --- a/docs/guide/examples/hooks_composed.rs +++ b/docs/guide/examples/hooks_composed.rs @@ -11,3 +11,57 @@ fn use_settings(cx: &ScopeState) -> &UseSharedState { use_shared_state::(cx).expect("App settings not provided") } // ANCHOR_END: wrap_context + +// ANCHOR: use_storage +use gloo_storage::{LocalStorage, Storage}; +use serde::{de::DeserializeOwned, Serialize}; + +/// A persistent storage hook that can be used to store data across application reloads. +#[allow(clippy::needless_return)] +pub fn use_persistent( + cx: &ScopeState, + // A unique key for the storage entry + key: impl ToString, + // A function that returns the initial value if the storage entry is empty + init: impl FnOnce() -> T, +) -> &UsePersistent { + // Use the use_ref hook to create a mutable state for the storage entry + let state = use_ref(cx, move || { + // This closure will run when the hook is created + let key = key.to_string(); + let value = LocalStorage::get(key.as_str()).ok().unwrap_or_else(init); + StorageEntry { key, value } + }); + + // Wrap the state in a new struct with a custom API + // Note: We use use_hook here so that this hook is easier to use in closures in the rsx. Any values with the same lifetime as the ScopeState can be used in the closure without cloning. + cx.use_hook(|| UsePersistent { + inner: state.clone(), + }) +} + +struct StorageEntry { + key: String, + value: T, +} + +/// Storage that persists across application reloads +pub struct UsePersistent { + inner: UseRef>, +} + +impl UsePersistent { + /// Returns a reference to the value + pub fn get(&self) -> T { + self.inner.read().value.clone() + } + + /// Sets the value + pub fn set(&self, value: T) { + let mut inner = self.inner.write(); + // Write the new value to local storage + LocalStorage::set(inner.key.as_str(), &value); + inner.value = value; + } +} +// ANCHOR_END: use_storage diff --git a/docs/guide/examples/hooks_custom_logic.rs b/docs/guide/examples/hooks_custom_logic.rs new file mode 100644 index 000000000..2323ddb11 --- /dev/null +++ b/docs/guide/examples/hooks_custom_logic.rs @@ -0,0 +1,57 @@ +#![allow(unused)] + +use dioxus::prelude::*; + +fn main() {} + +// ANCHOR: use_state +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +#[derive(Clone)] +struct UseState { + value: Rc>, + update: Arc, +} + +fn my_use_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &UseState { + cx.use_hook(|| { + // The update function will trigger a re-render in the component cx is attached to + let update = cx.schedule_update(); + // Create the initial state + let value = Rc::new(RefCell::new(init())); + + UseState { value, update } + }) +} + +impl UseState { + fn get(&self) -> T { + self.value.borrow().clone() + } + + fn set(&self, value: T) { + // Update the state + *self.value.borrow_mut() = value; + // Trigger a re-render on the component the state is from + (self.update)(); + } +} +// ANCHOR_END: use_state + +// ANCHOR: use_context +pub fn use_context(cx: &ScopeState) -> Option<&T> { + cx.use_hook(|| cx.consume_context::()).as_ref() +} + +pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) -> &T { + cx.use_hook(|| { + let val = f(); + // Provide the context state to the scope + cx.provide_context(val.clone()); + val + }) +} + +// ANCHOR_END: use_context diff --git a/docs/guide/src/en/interactivity/custom_hooks.md b/docs/guide/src/en/interactivity/custom_hooks.md index 07472bb95..f3acec269 100644 --- a/docs/guide/src/en/interactivity/custom_hooks.md +++ b/docs/guide/src/en/interactivity/custom_hooks.md @@ -2,6 +2,8 @@ Hooks are a great way to encapsulate business logic. If none of the existing hooks work for your problem, you can write your own. +When writing your hook, you can make a function that accepts `cx: &ScopeState` as a parameter to accept a scope with any Props. + ## Composing Hooks To avoid repetition, you can encapsulate business logic based on existing hooks to create a new hook. @@ -12,6 +14,12 @@ For example, if many components need to access an `AppSettings` struct, you can {{#include ../../../examples/hooks_composed.rs:wrap_context}} ``` +Or if you want to wrap a hook that persists reloads with the storage API, you can build on top of the use_ref hook to work with mutable state: + +```rust +{{#include ../../../examples/hooks_composed.rs:use_storage}} +``` + ## Custom Hook Logic You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on! @@ -23,4 +31,61 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct. Inside the initialization closure, you will typically make calls to other `cx` methods. For example: - The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes. + +Here is a simplified implementation of the `use_state` hook: + +```rust +{{#include ../../../examples/hooks_custom_logic.rs:use_state}} +``` + - The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope + +Here is an implementation of the `use_context` and `use_context_provider` hooks: + +```rust +{{#include ../../../examples/hooks_custom_logic.rs:use_context}} +``` + +## Hook Anti-Patterns + +When writing a custom hook, you should avoid the following anti-patterns: + +- !Clone Hooks: To allow hooks to be used within async blocks, the hooks must be Clone. To make a hook clone, you can wrap data in Rc or Arc and avoid lifetimes in hooks. + +This version of use_state may seem more efficient, but it is not cloneable: + +```rust +{{#include ../../../examples/hooks_anti_patterns.rs:non_clone_state}} +``` + +If we try to use this hook in an async block, we will get a compile error: + +```rust +fn FutureComponent(cx: &ScopeState) -> Element { + let my_state = my_use_state(cx, || 0); + cx.spawn({ + to_owned![my_state]; + async move { + my_state.set(1); + } + }); + + todo!() +} +``` + +But with the original version, we can use it in an async block: + +```rust +fn FutureComponent(cx: &ScopeState) -> Element { + let my_state = use_state(cx, || 0); + cx.spawn({ + to_owned![my_state]; + async move { + my_state.set(1); + } + }); + + todo!() +} +``` From f381e56ab396542bb4955c9afe3875dc67707b6b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 16:38:09 -0500 Subject: [PATCH 43/87] test scafolding --- packages/desktop/Cargo.toml | 6 ++++++ packages/desktop/headless_tests/main.rs | 28 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/desktop/headless_tests/main.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8b41019e1..af5f3e68f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -57,4 +57,10 @@ hot-reload = ["dioxus-hot-reload"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } +dioxus = { path = "../dioxus" } # image = "0.24.0" # enable this when generating a new desktop image + +[[test]] +name = "headless_tests" +path = "headless_tests/main.rs" +harness = false \ No newline at end of file diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs new file mode 100644 index 000000000..e07cb08c3 --- /dev/null +++ b/packages/desktop/headless_tests/main.rs @@ -0,0 +1,28 @@ +// Check that all events are being forwarded to the mock. +//! This example roughly shows how events are serialized into Rust from JavaScript. +//! +//! There is some conversion happening when input types are checkbox/radio/select/textarea etc. + +use dioxus::prelude::*; + +mod events; + +fn main() { + events::test_events(); +} + +pub(crate) fn check_app_exits(app: Component) { + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + panic!("App did not exit successfully") + } + }); + + dioxus_desktop::launch(app); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} From 01d673e6544c26395d8d719c19a507af2c95f611 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 18:00:43 -0500 Subject: [PATCH 44/87] create event tests --- packages/desktop/Cargo.toml | 2 +- packages/desktop/headless_tests/events.rs | 290 ++++++++++++++++++++++ packages/desktop/headless_tests/main.rs | 4 +- 3 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 packages/desktop/headless_tests/events.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index af5f3e68f..e8812df5a 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -58,7 +58,7 @@ hot-reload = ["dioxus-hot-reload"] dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } dioxus = { path = "../dioxus" } -# image = "0.24.0" # enable this when generating a new desktop image +exitcode = "1.1.2" [[test]] name = "headless_tests" diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs new file mode 100644 index 000000000..34e29f024 --- /dev/null +++ b/packages/desktop/headless_tests/events.rs @@ -0,0 +1,290 @@ +use crate::check_app_exits; +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; +use dioxus::html::geometry::euclid::Vector3D; + +pub fn test_events() { + check_app_exits(app); +} + +fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { + use_effect(cx, (), |_| { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + async move { + desktop_context.eval(&format!( + r#"let element = document.getElementById('{}'); + // Dispatch a synthetic event + const event = {}; + element.dispatchEvent(event); + "#, + id, value + )); + } + }); +} + +#[allow(deprecated)] +fn app(cx: Scope) -> Element { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + let recieved_events = use_state(cx, || 0); + + // button + mock_event( + &cx, + "button", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + button: 0, + })"#, + ); + // mouse_move_div + mock_event( + &cx, + "mouse_move_div", + r#"new MouseEvent("mousemove", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + })"#, + ); + // mouse_click_div + mock_event( + &cx, + "mouse_click_div", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"# + ); + // mouse_dblclick_div + mock_event( + &cx, + "mouse_dblclick_div", + r#"new MouseEvent("dblclick", { + view: window, + bubbles: true, + cancelable: true, + buttons: 1|2, + button: 2, + })"#, + ); + // mouse_down_div + mock_event( + &cx, + "mouse_down_div", + r#"new MouseEvent("mousedown", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"#, + ); + // mouse_up_div + mock_event( + &cx, + "mouse_up_div", + r#"new MouseEvent("mouseup", { + view: window, + bubbles: true, + cancelable: true, + buttons: 0, + button: 0, + })"#, + ); + // wheel_div + mock_event( + &cx, + "wheel_div", + r#"new WheelEvent("wheel", { + deltaX: 1.0, + deltaY: 2.0, + deltaZ: 3.0, + })"#, + ); + // key_down_div + mock_event( + &cx, + "key_down_div", + r#"new KeyboardEvent("keydown", { + key: "a", + code: "KeyA", + location: 0, + repeat: true, + })"#, + ); + // key_up_div + mock_event( + &cx, + "key_up_div", + r#"new KeyboardEvent("keyup", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + })"#, + ); + // key_press_div + mock_event( + &cx, + "key_press_div", + r#"new KeyboardEvent("keypress", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + })"#, + ); + // focus_in_div + mock_event( + &cx, + "focus_in_div", + r#"new FocusEvent("focusin")"#, + ); + // focus_out_div + mock_event( + &cx, + "focus_out_div", + r#"new FocusEvent("focusout")"#, + ); + + + if **recieved_events == 12 { + desktop_context.close(); + } + + cx.render(rsx! { + div { + button { + id: "button", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_move_div", + onmousemove: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_click_div", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div{ + id: "mouse_dblclick_div", + ondblclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_down_div", + onmousedown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_up_div", + onmouseup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "wheel_div", + onwheel: move |event| { + println!("{:?}", event.data); + let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ + panic!("Expected delta to be in pixels") + }; + assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_down_div", + onkeydown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_up_div", + onkeyup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_press_div", + onkeypress: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "focus_in_div", + onfocusin: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "focus_out_div", + onfocusout: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + } + }) +} diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs index e07cb08c3..65b6c80a3 100644 --- a/packages/desktop/headless_tests/main.rs +++ b/packages/desktop/headless_tests/main.rs @@ -16,9 +16,9 @@ pub(crate) fn check_app_exits(app: Component) { let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(100)); + std::thread::sleep(std::time::Duration::from_secs(10)); if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - panic!("App did not exit successfully") + std::process::exit(exitcode::SOFTWARE); } }); From 70687748d3577449d101e41a13a033c6cc76aa24 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 18:36:28 -0500 Subject: [PATCH 45/87] fix events test --- packages/desktop/headless_tests/events.rs | 55 ++++++++++++++++++++--- packages/desktop/headless_tests/main.rs | 2 +- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 34e29f024..60962c868 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -11,10 +11,12 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { use_effect(cx, (), |_| { let desktop_context: DesktopContext = cx.consume_context().unwrap(); async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; desktop_context.eval(&format!( r#"let element = document.getElementById('{}'); // Dispatch a synthetic event const event = {}; + console.log(element, event); element.dispatchEvent(event); "#, id, value @@ -103,9 +105,12 @@ fn app(cx: Scope) -> Element { &cx, "wheel_div", r#"new WheelEvent("wheel", { + view: window, deltaX: 1.0, deltaY: 2.0, deltaZ: 3.0, + deltaMode: 0x00, + bubbles: true, })"#, ); // key_down_div @@ -117,6 +122,17 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: true, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // key_up_div @@ -128,6 +144,17 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // key_press_div @@ -139,19 +166,30 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // focus_in_div mock_event( &cx, "focus_in_div", - r#"new FocusEvent("focusin")"#, + r#"new FocusEvent("focusin", {bubbles: true})"#, ); // focus_out_div mock_event( &cx, "focus_out_div", - r#"new FocusEvent("focusout")"#, + r#"new FocusEvent("focusout",{bubbles: true})"#, ); @@ -223,6 +261,9 @@ fn app(cx: Scope) -> Element { } div{ id: "wheel_div", + width: "100px", + height: "100px", + background_color: "red", onwheel: move |event| { println!("{:?}", event.data); let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ @@ -232,7 +273,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); @@ -245,7 +286,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); @@ -258,7 +299,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); @@ -271,14 +312,14 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "focus_out_div", onfocusout: move |event| { println!("{:?}", event.data); diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs index 65b6c80a3..a9d9ec61c 100644 --- a/packages/desktop/headless_tests/main.rs +++ b/packages/desktop/headless_tests/main.rs @@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) { let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(10)); + std::thread::sleep(std::time::Duration::from_secs(100)); if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { std::process::exit(exitcode::SOFTWARE); } From 0726a7b08f5e499869c114e37aae973d029d6ad3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 20:25:16 -0500 Subject: [PATCH 46/87] update server functions commit --- packages/server/Cargo.toml | 2 +- packages/server/server-macro/Cargo.toml | 2 +- packages/server/server-macro/src/lib.rs | 5 +- packages/server/src/adapters/axum_adapter.rs | 39 +++++++---- packages/server/src/adapters/salvo_adapter.rs | 44 ++++++++----- packages/server/src/adapters/warp_adapter.rs | 66 ++++++++++++++----- packages/server/src/lib.rs | 2 +- packages/server/src/server_fn.rs | 36 ++++++++-- 8 files changed, 138 insertions(+), 58 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 18633e97a..d182aa415 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"] [dependencies] # server functions -server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "a9e6590b5e7f1c0b01da7db7b86719cb18a4aaa1", features = ["stable"] } +server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] } dioxus_server_macro = { path = "server-macro" } # warp diff --git a/packages/server/server-macro/Cargo.toml b/packages/server/server-macro/Cargo.toml index 45b0ec3c6..286481bdd 100644 --- a/packages/server/server-macro/Cargo.toml +++ b/packages/server/server-macro/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] quote = "1.0.26" -server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "a9e6590b5e7f1c0b01da7db7b86719cb18a4aaa1", features = ["stable"] } +server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] } syn = { version = "1", features = ["full"] } [lib] diff --git a/packages/server/server-macro/src/lib.rs b/packages/server/server-macro/src/lib.rs index 14fc0ac77..b3f890f6a 100644 --- a/packages/server/server-macro/src/lib.rs +++ b/packages/server/server-macro/src/lib.rs @@ -14,9 +14,10 @@ use server_fn_macro::*; /// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered /// (e.g., `"/api"`). Defaults to `"/"`. /// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for -/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string). +/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string). /// Defaults to `"Url"`. If you want to use this server function to power a `` that will -/// work without WebAssembly, the encoding must be `"Url"`. +/// work without WebAssembly, the encoding must be `"Url"`. If you want to use this server function +/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`. /// /// The server function itself can take any number of arguments, each of which should be serializable /// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-server/latest/dixous_server/prelude/struct.DioxusServerContext.html), diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index f4bdd8a96..2d16e9491 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -55,10 +55,9 @@ //! } //! ``` -use std::{error::Error, sync::Arc}; - use axum::{ body::{self, Body, BoxBody, Full}, + extract::RawQuery, extract::{State, WebSocketUpgrade}, handler::Handler, http::{HeaderMap, Request, Response, StatusCode}, @@ -66,14 +65,13 @@ use axum::{ routing::{get, post}, Router, }; -use server_fn::{Payload, ServerFunctionRegistry}; +use server_fn::{Encoding, Payload, ServerFunctionRegistry}; +use std::error::Error; use tokio::task::spawn_blocking; use crate::{ - render::SSRState, - serve_config::ServeConfig, - server_context::DioxusServerContext, - server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, + prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext, + server_fn::DioxusServerFnRegistry, }; /// A extension trait with utilities for integrating Dioxus with your Axum router. @@ -106,7 +104,7 @@ pub trait DioxusRouterExt { fn register_server_fns_with_handler( self, server_fn_route: &'static str, - handler: impl Fn(Arc) -> H, + handler: impl Fn(ServerFunction) -> H, ) -> Self where H: Handler, @@ -200,7 +198,7 @@ where fn register_server_fns_with_handler( self, server_fn_route: &'static str, - mut handler: impl FnMut(Arc) -> H, + mut handler: impl FnMut(ServerFunction) -> H, ) -> Self where H: Handler, @@ -211,15 +209,22 @@ where for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.route(&full_route, post(handler(func))); + match func.encoding { + Encoding::Url | Encoding::Cbor => { + router = router.route(&full_route, post(handler(func))); + } + Encoding::GetJSON | Encoding::GetCBOR => { + router = router.route(&full_route, get(handler(func))); + } + } } router } fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { - move |headers: HeaderMap, body: Request| async move { - server_fn_handler((), func.clone(), headers, body).await + move |headers: HeaderMap, RawQuery(query): RawQuery, body: Request| async move { + server_fn_handler((), func.clone(), headers, query, body).await } }) } @@ -299,8 +304,9 @@ async fn render_handler( /// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub async fn server_fn_handler( server_context: impl Into, - function: Arc, + function: ServerFunction, headers: HeaderMap, + query: Option, req: Request, ) -> impl IntoResponse { let server_context = server_context.into(); @@ -317,7 +323,12 @@ pub async fn server_fn_handler( tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") .block_on(async { - let resp = match function(server_context, &body).await { + let query = &query.unwrap_or_default().into(); + let data = match &function.encoding { + Encoding::Url | Encoding::Cbor => &body, + Encoding::GetJSON | Encoding::GetCBOR => query, + }; + let resp = match (function.trait_obj)(server_context, &data).await { Ok(serialized) => { // if this is Accept: application/json then send a serialized JSON response let accept_header = diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index ffe72bef5..402a0b0a0 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -50,7 +50,7 @@ //! } //! ``` -use std::{error::Error, sync::Arc}; +use std::error::Error; use hyper::{http::HeaderValue, StatusCode}; use salvo::{ @@ -58,14 +58,11 @@ use salvo::{ serve_static::{StaticDir, StaticFile}, Depot, FlowCtrl, Handler, Request, Response, Router, }; -use server_fn::{Payload, ServerFunctionRegistry}; +use server_fn::{Encoding, Payload, ServerFunctionRegistry}; use tokio::task::spawn_blocking; use crate::{ - prelude::DioxusServerContext, - render::SSRState, - serve_config::ServeConfig, - server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, + prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry, }; /// A extension trait with utilities for integrating Dioxus with your Salvo router. @@ -79,7 +76,7 @@ pub trait DioxusRouterExt { /// use dioxus_server::prelude::*; /// /// struct ServerFunctionHandler { - /// server_fn: Arc, + /// server_fn: ServerFunction, /// } /// /// #[handler] @@ -112,7 +109,7 @@ pub trait DioxusRouterExt { fn register_server_fns_with_handler( self, server_fn_route: &'static str, - handler: impl Fn(Arc) -> H, + handler: impl Fn(ServerFunction) -> H, ) -> Self where H: Handler + 'static; @@ -187,7 +184,7 @@ impl DioxusRouterExt for Router { fn register_server_fns_with_handler( self, server_fn_route: &'static str, - mut handler: impl FnMut(Arc) -> H, + mut handler: impl FnMut(ServerFunction) -> H, ) -> Self where H: Handler + 'static, @@ -196,7 +193,14 @@ impl DioxusRouterExt for Router { for server_fn_path in DioxusServerFnRegistry::paths_registered() { let func = DioxusServerFnRegistry::get(server_fn_path).unwrap(); let full_route = format!("{server_fn_route}/{server_fn_path}"); - router = router.push(Router::with_path(&full_route).post(handler(func))); + match func.encoding { + Encoding::Url | Encoding::Cbor => { + router = router.push(Router::with_path(&full_route).post(handler(func))); + } + Encoding::GetJSON | Encoding::GetCBOR => { + router = router.push(Router::with_path(&full_route).get(handler(func))); + } + } } router } @@ -288,15 +292,12 @@ impl Handler for SSRHandler

{ /// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub struct ServerFnHandler { server_context: DioxusServerContext, - function: Arc, + function: ServerFunction, } impl ServerFnHandler { /// Create a new server function handler with the given server context and server function. - pub fn new( - server_context: impl Into, - function: Arc, - ) -> Self { + pub fn new(server_context: impl Into, function: ServerFunction) -> Self { let server_context = server_context.into(); Self { server_context, @@ -324,12 +325,23 @@ impl ServerFnHandler { let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); let function = function.clone(); let server_context = server_context.clone(); + let query = req + .uri() + .query() + .unwrap_or_default() + .as_bytes() + .to_vec() + .into(); spawn_blocking({ move || { tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") .block_on(async move { - let resp = function(server_context, &body).await; + let data = match &function.encoding { + Encoding::Url | Encoding::Cbor => &body, + Encoding::GetJSON | Encoding::GetCBOR => &query, + }; + let resp = (function.trait_obj)(server_context, data).await; resp_tx.send(resp).unwrap(); }) diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 5041e294d..db88ac2b4 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -47,9 +47,12 @@ //! //! ``` -use std::{error::Error, sync::Arc}; +use crate::{ + prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry, +}; -use server_fn::{Payload, ServerFunctionRegistry}; +use server_fn::{Encoding, Payload, ServerFunctionRegistry}; +use std::error::Error; use tokio::task::spawn_blocking; use warp::{ filters::BoxedFilter, @@ -58,13 +61,6 @@ use warp::{ path, Filter, Reply, }; -use crate::{ - prelude::DioxusServerContext, - render::SSRState, - serve_config::ServeConfig, - server_fn::{DioxusServerFnRegistry, ServerFnTraitObj}, -}; - /// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request. /// /// # Example @@ -94,7 +90,7 @@ pub fn register_server_fns_with_handler( mut handler: H, ) -> BoxedFilter<(R,)> where - H: FnMut(String, Arc) -> F, + H: FnMut(String, ServerFunction) -> F, F: Filter + Send + Sync + 'static, F::Extract: Send, R: Reply + 'static, @@ -129,16 +125,48 @@ where /// ``` pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { register_server_fns_with_handler(server_fn_route, |full_route, func| { - path(full_route) - .and(warp::post()) + let func2 = func.clone(); + let func3 = func.clone(); + path(full_route.clone()) + .and(warp::filters::method::get()) .and(warp::header::headers_cloned()) + .and(warp::filters::query::raw()) .and(warp::body::bytes()) - .and_then(move |headers: HeaderMap, body| { + .and_then(move |headers, query, body| { let func = func.clone(); async move { - server_fn_handler(DioxusServerContext::default(), func, headers, body).await + server_fn_handler( + DioxusServerContext::default(), + func, + headers, + Some(query), + body, + ) + .await } }) + .or(path(full_route.clone()) + .and(warp::filters::method::get()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and_then(move |headers, body| { + let func = func2.clone(); + async move { + server_fn_handler(DioxusServerContext::default(), func, headers, None, body) + .await + } + })) + .or(path(full_route) + .and(warp::filters::method::post()) + .and(warp::header::headers_cloned()) + .and(warp::body::bytes()) + .and_then(move |headers, body| { + let func = func3.clone(); + async move { + server_fn_handler(DioxusServerContext::default(), func, headers, None, body) + .await + } + })) }) } @@ -198,8 +226,9 @@ impl warp::reject::Reject for RecieveFailed {} /// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub async fn server_fn_handler( server_context: impl Into, - function: Arc, + function: ServerFunction, headers: HeaderMap, + query: Option, body: Bytes, ) -> Result, warp::Rejection> { let server_context = server_context.into(); @@ -210,7 +239,12 @@ pub async fn server_fn_handler( tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") .block_on(async { - let resp = match function(server_context, &body).await { + let query = &query.unwrap_or_default().into(); + let data = match &function.encoding { + Encoding::Url | Encoding::Cbor => &body, + Encoding::GetJSON | Encoding::GetCBOR => query, + }; + let resp = match (function.trait_obj)(server_context, &data).await { Ok(serialized) => { // if this is Accept: application/json then send a serialized JSON response let accept_header = diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index df2735cf4..525b636b3 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -30,7 +30,7 @@ pub mod prelude { pub use crate::server_context::DioxusServerContext; pub use crate::server_fn::ServerFn; #[cfg(feature = "ssr")] - pub use crate::server_fn::ServerFnTraitObj; + pub use crate::server_fn::{ServerFnTraitObj, ServerFunction}; pub use dioxus_server_macro::*; pub use server_fn::{self, ServerFn as _, ServerFnError}; } diff --git a/packages/server/src/server_fn.rs b/packages/server/src/server_fn.rs index 643f25c9f..da7311263 100644 --- a/packages/server/src/server_fn.rs +++ b/packages/server/src/server_fn.rs @@ -4,14 +4,14 @@ use crate::server_context::DioxusServerContext; /// A trait object for a function that be called on serializable arguments and returns a serializable result. pub type ServerFnTraitObj = server_fn::ServerFnTraitObj; +#[cfg(any(feature = "ssr", doc))] +/// A server function that can be called on serializable arguments and returns a serializable result. +pub type ServerFunction = server_fn::ServerFunction; + #[cfg(any(feature = "ssr", doc))] #[allow(clippy::type_complexity)] static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy< - std::sync::Arc< - std::sync::RwLock< - std::collections::HashMap<&'static str, std::sync::Arc>, - >, - >, + std::sync::Arc>>, > = once_cell::sync::Lazy::new(Default::default); #[cfg(any(feature = "ssr", doc))] @@ -25,12 +25,19 @@ impl server_fn::ServerFunctionRegistry for DioxusServerFnRe fn register( url: &'static str, server_function: std::sync::Arc, + encoding: server_fn::Encoding, ) -> Result<(), Self::Error> { // store it in the hashmap let mut write = REGISTERED_SERVER_FUNCTIONS .write() .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; - let prev = write.insert(url, server_function); + let prev = write.insert( + url, + ServerFunction { + trait_obj: server_function, + encoding, + }, + ); // if there was already a server function with this key, // return Err @@ -47,13 +54,28 @@ impl server_fn::ServerFunctionRegistry for DioxusServerFnRe } /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. - fn get(url: &str) -> Option> { + fn get(url: &str) -> Option { REGISTERED_SERVER_FUNCTIONS .read() .ok() .and_then(|fns| fns.get(url).cloned()) } + /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. + fn get_trait_obj(url: &str) -> Option> { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .and_then(|fns| fns.get(url).map(|f| f.trait_obj.clone())) + } + + fn get_encoding(url: &str) -> Option { + REGISTERED_SERVER_FUNCTIONS + .read() + .ok() + .and_then(|fns| fns.get(url).map(|f| f.encoding.clone())) + } + /// Returns a list of all registered server functions. fn paths_registered() -> Vec<&'static str> { REGISTERED_SERVER_FUNCTIONS From 03a282466026b0bb68b9e6581c3e6ccec9fd0ac8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 28 Apr 2023 17:59:01 -0500 Subject: [PATCH 47/87] allow passing root prop data from the server to the client --- packages/server/Cargo.toml | 5 ++ .../examples/axum-hello-world/src/main.rs | 21 ++++- .../examples/salvo-hello-world/src/main.rs | 22 +++-- .../examples/warp-hello-world/src/main.rs | 21 ++++- packages/server/src/adapters/axum_adapter.rs | 6 +- packages/server/src/adapters/salvo_adapter.rs | 6 +- packages/server/src/adapters/warp_adapter.rs | 2 +- packages/server/src/lib.rs | 4 + .../src/props_html/deserialize_props.rs | 37 ++++++++ packages/server/src/props_html/mod.rs | 85 +++++++++++++++++++ .../server/src/props_html/serialize_props.rs | 37 ++++++++ packages/server/src/render.rs | 5 +- 12 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 packages/server/src/props_html/deserialize_props.rs create mode 100644 packages/server/src/props_html/mod.rs create mode 100644 packages/server/src/props_html/serialize_props.rs diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index d182aa415..2e20cdc81 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -44,10 +44,15 @@ anymap = "0.12.1" serde_json = { version = "1.0.95", optional = true } tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } futures-util = { version = "0.3.28", optional = true } +postcard = { version = "1.0.4", features = ["use-std"] } +yazi = "0.1.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { path = "../hot-reload" } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } + [features] default = ["hot-reload"] hot-reload = ["serde_json", "tokio-stream", "futures-util"] diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 899247af5..b5553f7a2 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { PostServerData::register().unwrap(); @@ -23,7 +28,10 @@ fn main() { axum::Server::bind(&addr) .serve( axum::Router::new() - .serve_dioxus_application("", ServeConfigBuilder::new(app, ())) + .serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(), + ) .into_make_service(), ) .await @@ -32,8 +40,13 @@ fn main() { } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index d1e654fee..15a3ebe31 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { use salvo::prelude::*; @@ -20,8 +25,10 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let router = - Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ())); + let router = Router::new().serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }), + ); Server::new(TcpListener::bind("127.0.0.1:8080")) .serve(router) .await; @@ -29,8 +36,13 @@ fn main() { } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 51a627d16..a636dbc58 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -8,10 +8,15 @@ #![allow(non_snake_case)] use dioxus::prelude::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] - dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true)); + dioxus_web::launch_with_props( + app, + get_props_from_document().unwrap_or_default(), + dioxus_web::Config::new().hydrate(true), + ); #[cfg(feature = "ssr")] { PostServerData::register().unwrap(); @@ -19,14 +24,22 @@ fn main() { tokio::runtime::Runtime::new() .unwrap() .block_on(async move { - let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ())); + let routes = serve_dioxus_application( + "", + ServeConfigBuilder::new(app, AppProps { count: 12345 }), + ); warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; }); } } -fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); +#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] +struct AppProps { + count: i32, +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 2d16e9491..7fc47a8b3 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -184,7 +184,7 @@ pub trait DioxusRouterExt { /// todo!() /// } /// ``` - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_route: &'static str, cfg: impl Into>, @@ -229,7 +229,7 @@ where }) } - fn serve_dioxus_application( + fn serve_dioxus_application( mut self, server_fn_route: &'static str, cfg: impl Into>, @@ -294,7 +294,7 @@ where } } -async fn render_handler( +async fn render_handler( State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, ) -> impl IntoResponse { let rendered = ssr_state.render(&cfg); diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 402a0b0a0..ca11ff9ec 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -173,7 +173,7 @@ pub trait DioxusRouterExt { /// /// fn app(cx: Scope) -> Element {todo!()} /// ``` - fn serve_dioxus_application( + fn serve_dioxus_application( self, server_fn_path: &'static str, cfg: impl Into>, @@ -212,7 +212,7 @@ impl DioxusRouterExt for Router { }) } - fn serve_dioxus_application( + fn serve_dioxus_application( mut self, server_fn_route: &'static str, cfg: impl Into>, @@ -269,7 +269,7 @@ struct SSRHandler { } #[async_trait] -impl Handler for SSRHandler

{ +impl Handler for SSRHandler

{ async fn handle( &self, _req: &mut Request, diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index db88ac2b4..414160045 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -189,7 +189,7 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R /// todo!() /// } /// ``` -pub fn serve_dioxus_application( +pub fn serve_dioxus_application( server_fn_route: &'static str, cfg: impl Into>, ) -> BoxedFilter<(impl Reply,)> { diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index 525b636b3..e9f4ee63f 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -5,6 +5,8 @@ pub use adapters::*; +mod props_html; + mod adapters; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; @@ -23,6 +25,8 @@ pub mod prelude { pub use crate::adapters::salvo_adapter::*; #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; + #[cfg(not(feature = "ssr"))] + pub use crate::props_html::deserialize_props::get_props_from_document; #[cfg(feature = "ssr")] pub use crate::render::SSRState; #[cfg(feature = "ssr")] diff --git a/packages/server/src/props_html/deserialize_props.rs b/packages/server/src/props_html/deserialize_props.rs new file mode 100644 index 000000000..28b206ac5 --- /dev/null +++ b/packages/server/src/props_html/deserialize_props.rs @@ -0,0 +1,37 @@ +use serde::de::DeserializeOwned; + +use super::u16_from_char; + +#[allow(unused)] +pub(crate) fn serde_from_string(string: &str) -> Option { + let decompressed = string + .chars() + .flat_map(|c| { + let u = u16_from_char(c); + let u1 = (u >> 8) as u8; + let u2 = (u & 0xFF) as u8; + [u1, u2].into_iter() + }) + .collect::>(); + let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap(); + + postcard::from_bytes(&decompressed).ok() +} + +#[cfg(not(feature = "ssr"))] +/// Get the props from the document. This is only available in the browser. +pub fn get_props_from_document() -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + None + } + #[cfg(target_arch = "wasm32")] + { + let attribute = web_sys::window()? + .document()? + .get_element_by_id("dioxus-storage")? + .get_attribute("data-serialized")?; + + serde_from_string(&attribute) + } +} diff --git a/packages/server/src/props_html/mod.rs b/packages/server/src/props_html/mod.rs new file mode 100644 index 000000000..9e9b8721e --- /dev/null +++ b/packages/server/src/props_html/mod.rs @@ -0,0 +1,85 @@ +pub(crate) mod deserialize_props; + +pub(crate) mod serialize_props; + +#[test] +fn serialized_and_deserializes() { + use postcard::to_allocvec; + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Data { + a: u32, + b: String, + bytes: Vec, + nested: Nested, + } + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)] + struct Nested { + a: u32, + b: u16, + c: u8, + } + + for x in 0..10usize { + for y in 0..10 { + let mut as_string = String::new(); + let data = vec![ + Data { + a: x as u32, + b: "hello".to_string(), + bytes: vec![0; x], + nested: Nested { + a: 1, + b: x as u16, + c: 3 + }, + }; + y + ]; + serialize_props::serde_to_writable(&data, &mut as_string).unwrap(); + + println!("{}", as_string); + println!( + "original size: {}", + std::mem::size_of::() * data.len() + ); + println!("serialized size: {}", to_allocvec(&data).unwrap().len()); + println!("compressed size: {}", as_string.len()); + + let decoded: Vec = deserialize_props::serde_from_string(&as_string).unwrap(); + assert_eq!(data, decoded); + } + } +} + +#[test] +fn encodes_and_decodes_bytes() { + for i in 0..(u16::MAX) { + let c = u16_to_char(i); + let i2 = u16_from_char(c); + assert_eq!(i, i2); + } +} + +#[allow(unused)] +pub(crate) fn u16_to_char(u: u16) -> char { + let u = u as u32; + let mapped = if u <= 0xD7FF { + u + } else { + 0xE000 + (u - 0xD7FF) + }; + char::from_u32(mapped).unwrap() +} + +#[allow(unused)] +pub(crate) fn u16_from_char(c: char) -> u16 { + let c = c as u32; + let mapped = if c <= 0xD7FF { + c + } else { + 0xD7FF + (c - 0xE000) + }; + mapped as u16 +} diff --git a/packages/server/src/props_html/serialize_props.rs b/packages/server/src/props_html/serialize_props.rs new file mode 100644 index 000000000..f1282f07f --- /dev/null +++ b/packages/server/src/props_html/serialize_props.rs @@ -0,0 +1,37 @@ +use serde::Serialize; + +use super::u16_to_char; + +#[allow(unused)] +pub(crate) fn serde_to_writable( + value: &T, + mut write_to: impl std::fmt::Write, +) -> std::fmt::Result { + let serialized = postcard::to_allocvec(value).unwrap(); + let compressed = yazi::compress( + &serialized, + yazi::Format::Zlib, + yazi::CompressionLevel::BestSize, + ) + .unwrap(); + for array in compressed.chunks(2) { + let w = if array.len() == 2 { + [array[0], array[1]] + } else { + [array[0], 0] + }; + write_to.write_char(u16_to_char((w[0] as u16) << 8 | (w[1] as u16)))?; + } + Ok(()) +} + +#[cfg(feature = "ssr")] +/// Encode data into a element. This is inteded to be used in the server to send data to the client. +pub(crate) fn encode_in_element( + data: T, + mut write_to: impl std::fmt::Write, +) -> std::fmt::Result { + write_to.write_str(r#""#) +} diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 8328be317..0f61a3a0f 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -24,7 +24,7 @@ impl Default for SSRState { impl SSRState { /// Render the application to HTML. - pub fn render(&self, cfg: &ServeConfig

) -> String { + pub fn render(&self, cfg: &ServeConfig

) -> String { let ServeConfig { app, props, index, .. } = cfg; @@ -41,6 +41,9 @@ impl SSRState { let _ = renderer.render_to(&mut html, &vdom); + // serialize the props + let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); + html += &index.post_main; html From 581a4648de58f3db9a5b617a39a9b73d7f639bf9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 28 Apr 2023 18:01:21 -0500 Subject: [PATCH 48/87] document get_root_props_from_document --- packages/server/examples/axum-hello-world/src/main.rs | 2 +- packages/server/examples/salvo-hello-world/src/main.rs | 2 +- packages/server/examples/warp-hello-world/src/main.rs | 2 +- packages/server/src/lib.rs | 2 +- packages/server/src/props_html/deserialize_props.rs | 4 +++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index b5553f7a2..5b3323ec4 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -14,7 +14,7 @@ fn main() { #[cfg(feature = "web")] dioxus_web::launch_with_props( app, - get_props_from_document().unwrap_or_default(), + get_root_props_from_document().unwrap_or_default(), dioxus_web::Config::new().hydrate(true), ); #[cfg(feature = "ssr")] diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index 15a3ebe31..a6e33dc63 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -14,7 +14,7 @@ fn main() { #[cfg(feature = "web")] dioxus_web::launch_with_props( app, - get_props_from_document().unwrap_or_default(), + get_root_props_from_document().unwrap_or_default(), dioxus_web::Config::new().hydrate(true), ); #[cfg(feature = "ssr")] diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index a636dbc58..4929af0c8 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -14,7 +14,7 @@ fn main() { #[cfg(feature = "web")] dioxus_web::launch_with_props( app, - get_props_from_document().unwrap_or_default(), + get_root_props_from_document().unwrap_or_default(), dioxus_web::Config::new().hydrate(true), ); #[cfg(feature = "ssr")] diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index e9f4ee63f..c5ad196bc 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -26,7 +26,7 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(not(feature = "ssr"))] - pub use crate::props_html::deserialize_props::get_props_from_document; + pub use crate::props_html::deserialize_props::get_root_props_from_document; #[cfg(feature = "ssr")] pub use crate::render::SSRState; #[cfg(feature = "ssr")] diff --git a/packages/server/src/props_html/deserialize_props.rs b/packages/server/src/props_html/deserialize_props.rs index 28b206ac5..64f2181ab 100644 --- a/packages/server/src/props_html/deserialize_props.rs +++ b/packages/server/src/props_html/deserialize_props.rs @@ -20,7 +20,9 @@ pub(crate) fn serde_from_string(string: &str) -> Option #[cfg(not(feature = "ssr"))] /// Get the props from the document. This is only available in the browser. -pub fn get_props_from_document() -> Option { +/// +/// When dioxus-server renders the page, it will serialize the root props and put them in the document. This function gets them from the document. +pub fn get_root_props_from_document() -> Option { #[cfg(not(target_arch = "wasm32"))] { None From 7ff5d356d5cdfbf2709fb7120e7f7088dcb6a658 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 29 Apr 2023 17:04:54 -0500 Subject: [PATCH 49/87] allow server functions to read request and modify responce --- packages/server/Cargo.toml | 9 +- .../examples/axum-hello-world/Cargo.toml | 7 + .../examples/axum-hello-world/src/main.rs | 9 +- .../server/examples/axum-router/Cargo.toml | 7 + .../examples/salvo-hello-world/Cargo.toml | 7 + .../examples/salvo-hello-world/src/main.rs | 11 +- .../examples/warp-hello-world/Cargo.toml | 7 + .../examples/warp-hello-world/src/main.rs | 11 +- packages/server/src/adapters/axum_adapter.rs | 46 ++-- packages/server/src/adapters/salvo_adapter.rs | 62 ++++-- packages/server/src/adapters/warp_adapter.rs | 145 ++++++++----- packages/server/src/lib.rs | 6 +- packages/server/src/render.rs | 15 +- packages/server/src/server_context.rs | 204 +++++++++++++----- 14 files changed, 387 insertions(+), 159 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index 2e20cdc81..b6293993c 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -23,7 +23,6 @@ http-body = { version = "0.4.5", optional = true } # axum axum = { version = "0.6.1", features = ["ws"], optional = true } tower-http = { version = "0.4.0", optional = true, features = ["fs"] } -hyper = { version = "0.14.25", optional = true } axum-macros = "0.3.7" # salvo @@ -33,6 +32,8 @@ serde = "1.0.159" # Dioxus + SSR dioxus-core = { path = "../core", version = "^0.3.0" } dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true } +hyper = { version = "0.14.25", optional = true } +http = { version = "0.2.9", optional = true } log = "0.4.17" once_cell = "1.17.1" @@ -57,6 +58,6 @@ web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "Ht default = ["hot-reload"] hot-reload = ["serde_json", "tokio-stream", "futures-util"] warp = ["dep:warp", "http-body", "ssr"] -axum = ["dep:axum", "tower-http", "hyper", "ssr"] -salvo = ["dep:salvo", "hyper", "ssr"] -ssr = ["server_fn/ssr", "tokio", "dioxus-ssr"] +axum = ["dep:axum", "tower-http", "ssr"] +salvo = ["dep:salvo", "ssr"] +ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"] diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index fd1f051d1..b2c14561d 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -18,3 +18,10 @@ serde = "1.0.159" default = ["web"] ssr = ["axum", "tokio", "dioxus-server/axum"] web = ["dioxus-web"] + +[profile.release] +lto = true +panic = "abort" +opt-level = 3 +strip = true +codegen-units = 1 diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 5b3323ec4..6b6f32db7 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -56,11 +56,12 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| { to_owned![text]; + let sc = cx.sc(); async move { if let Ok(data) = get_server_data().await { println!("Client received: {}", data); text.set(data.clone()); - post_server_data(data).await.unwrap(); + post_server_data(sc, data).await.unwrap(); } } }, @@ -71,8 +72,12 @@ fn app(cx: Scope) -> Element { } #[server(PostServerData)] -async fn post_server_data(data: String) -> Result<(), ServerFnError> { +async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> { + // The server context contains information about the current request and allows you to modify the response. + cx.responce_headers_mut() + .insert("Set-Cookie", "foo=bar".parse().unwrap()); println!("Server received: {}", data); + println!("Request parts are {:?}", cx.request_parts()); Ok(()) } diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml index 699779763..3e137d8f6 100644 --- a/packages/server/examples/axum-router/Cargo.toml +++ b/packages/server/examples/axum-router/Cargo.toml @@ -21,3 +21,10 @@ http = { version = "0.2.9", optional = true } default = ["web"] ssr = ["axum", "tokio", "dioxus-server/axum", "tower-http", "http"] web = ["dioxus-web", "dioxus-router/web"] + +[profile.release] +lto = true +panic = "abort" +opt-level = 3 +strip = true +codegen-units = 1 diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 1ddf52915..9146ce3e2 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -18,3 +18,10 @@ salvo = { version = "0.37.9", optional = true } default = ["web"] ssr = ["salvo", "tokio", "dioxus-server/salvo"] web = ["dioxus-web"] + +[profile.release] +lto = true +panic = "abort" +opt-level = 3 +strip = true +codegen-units = 1 diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index a6e33dc63..ff1081077 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -44,6 +44,7 @@ struct AppProps { fn app(cx: Scope) -> Element { let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); + let server_context = cx.sc(); cx.render(rsx! { h1 { "High-Five counter: {count}" } @@ -51,12 +52,12 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| count -= 1, "Down low!" } button { onclick: move |_| { - to_owned![text]; + to_owned![text, server_context]; async move { if let Ok(data) = get_server_data().await { println!("Client received: {}", data); text.set(data.clone()); - post_server_data(data).await.unwrap(); + post_server_data(server_context, data).await.unwrap(); } } }, @@ -67,8 +68,12 @@ fn app(cx: Scope) -> Element { } #[server(PostServerData)] -async fn post_server_data(data: String) -> Result<(), ServerFnError> { +async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> { + // The server context contains information about the current request and allows you to modify the response. + cx.responce_headers_mut() + .insert("Set-Cookie", "foo=bar".parse().unwrap()); println!("Server received: {}", data); + println!("Request parts are {:?}", cx.request_parts()); Ok(()) } diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index cbd8606dd..e182494b0 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -18,3 +18,10 @@ warp = { version = "0.3.3", optional = true } default = ["web"] ssr = ["warp", "tokio", "dioxus-server/warp"] web = ["dioxus-web"] + +[profile.release] +lto = true +panic = "abort" +opt-level = 3 +strip = true +codegen-units = 1 diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 4929af0c8..04684f803 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -41,6 +41,7 @@ struct AppProps { fn app(cx: Scope) -> Element { let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); + let server_context = cx.sc(); cx.render(rsx! { h1 { "High-Five counter: {count}" } @@ -48,12 +49,12 @@ fn app(cx: Scope) -> Element { button { onclick: move |_| count -= 1, "Down low!" } button { onclick: move |_| { - to_owned![text]; + to_owned![text, server_context]; async move { if let Ok(data) = get_server_data().await { println!("Client received: {}", data); text.set(data.clone()); - post_server_data(data).await.unwrap(); + post_server_data(server_context, data).await.unwrap(); } } }, @@ -64,8 +65,12 @@ fn app(cx: Scope) -> Element { } #[server(PostServerData)] -async fn post_server_data(data: String) -> Result<(), ServerFnError> { +async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> { + // The server context contains information about the current request and allows you to modify the response. + cx.responce_headers_mut() + .insert("Set-Cookie", "foo=bar".parse().unwrap()); println!("Server received: {}", data); + println!("Request parts are {:?}", cx.request_parts()); Ok(()) } diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 7fc47a8b3..1b572c804 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -57,16 +57,17 @@ use axum::{ body::{self, Body, BoxBody, Full}, - extract::RawQuery, extract::{State, WebSocketUpgrade}, handler::Handler, - http::{HeaderMap, Request, Response, StatusCode}, + http::{Request, Response, StatusCode}, response::IntoResponse, routing::{get, post}, Router, }; +use dioxus_core::VirtualDom; use server_fn::{Encoding, Payload, ServerFunctionRegistry}; use std::error::Error; +use std::sync::Arc; use tokio::task::spawn_blocking; use crate::{ @@ -223,8 +224,11 @@ where fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { - move |headers: HeaderMap, RawQuery(query): RawQuery, body: Request| async move { - server_fn_handler((), func.clone(), headers, query, body).await + move |req: Request| async move { + let (parts, body) = req.into_parts(); + let parts: Arc = Arc::new(parts.into()); + let server_context = DioxusServerContext::new(parts.clone()); + server_fn_handler(server_context, func.clone(), parts, body).await } }) } @@ -296,44 +300,54 @@ where async fn render_handler( State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, + request: Request, ) -> impl IntoResponse { - let rendered = ssr_state.render(&cfg); + let (parts, _) = request.into_parts(); + let parts: Arc = Arc::new(parts.into()); + let server_context = DioxusServerContext::new(parts); + let mut vdom = + VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context); + let _ = vdom.rebuild(); + + let rendered = ssr_state.render_vdom(&vdom, &cfg); Full::from(rendered) } /// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response. pub async fn server_fn_handler( - server_context: impl Into, + server_context: DioxusServerContext, function: ServerFunction, - headers: HeaderMap, - query: Option, - req: Request, + parts: Arc, + body: Body, ) -> impl IntoResponse { - let server_context = server_context.into(); - let (_, body) = req.into_parts(); let body = hyper::body::to_bytes(body).await; - let Ok(body)=body else { + let Ok(body) = body else { return report_err(body.err().unwrap()); }; // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + let query_string = parts.uri.query().unwrap_or_default().to_string(); spawn_blocking({ move || { tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") .block_on(async { - let query = &query.unwrap_or_default().into(); + let query = &query_string.into(); let data = match &function.encoding { Encoding::Url | Encoding::Cbor => &body, Encoding::GetJSON | Encoding::GetCBOR => query, }; - let resp = match (function.trait_obj)(server_context, &data).await { + let resp = match (function.trait_obj)(server_context.clone(), &data).await { Ok(serialized) => { // if this is Accept: application/json then send a serialized JSON response - let accept_header = - headers.get("Accept").and_then(|value| value.to_str().ok()); + let accept_header = parts + .headers + .get("Accept") + .and_then(|value| value.to_str().ok()); let mut res = Response::builder(); + *res.headers_mut().expect("empty responce should be valid") = + server_context.take_responce_headers(); if accept_header == Some("application/json") || accept_header == Some( diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index ca11ff9ec..6ed4c2b3e 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -50,8 +50,7 @@ //! } //! ``` -use std::error::Error; - +use dioxus_core::VirtualDom; use hyper::{http::HeaderValue, StatusCode}; use salvo::{ async_trait, handler, @@ -59,6 +58,8 @@ use salvo::{ Depot, FlowCtrl, Handler, Request, Response, Router, }; use server_fn::{Encoding, Payload, ServerFunctionRegistry}; +use std::error::Error; +use std::sync::Arc; use tokio::task::spawn_blocking; use crate::{ @@ -172,7 +173,7 @@ pub trait DioxusRouterExt { /// } /// /// fn app(cx: Scope) -> Element {todo!()} - /// ``` + /// ``` fn serve_dioxus_application( self, server_fn_path: &'static str, @@ -264,6 +265,17 @@ impl DioxusRouterExt for Router { } } +/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values. +pub fn extract_parts(req: &mut Request) -> RequestParts { + RequestParts { + method: std::mem::take(req.method_mut()), + uri: std::mem::take(req.uri_mut()), + version: req.version(), + headers: std::mem::take(req.headers_mut()), + extensions: std::mem::take(req.extensions_mut()), + } +} + struct SSRHandler { cfg: ServeConfig

, } @@ -272,7 +284,7 @@ struct SSRHandler { impl Handler for SSRHandler

{ async fn handle( &self, - _req: &mut Request, + req: &mut Request, depot: &mut Depot, res: &mut Response, _flow: &mut FlowCtrl, @@ -285,7 +297,16 @@ impl Handler for SSRHandler depot.inject(renderer.clone()); renderer }; - res.write_body(renderer_pool.render(&self.cfg)).unwrap(); + let parts: Arc = Arc::new(extract_parts(req)); + let server_context = DioxusServerContext::new(parts); + let mut vdom = VirtualDom::new_with_props(self.cfg.app, self.cfg.props.clone()) + .with_root_context(server_context.clone()); + let _ = vdom.rebuild(); + + res.write_body(renderer_pool.render_vdom(&vdom, &self.cfg)) + .unwrap(); + + *res.headers_mut() = server_context.take_responce_headers(); } } @@ -314,17 +335,6 @@ impl ServerFnHandler { function, } = self; - let body = hyper::body::to_bytes(req.body_mut().unwrap()).await; - let Ok(body)=body else { - handle_error(body.err().unwrap(), res); - return; - }; - let headers = req.headers(); - - // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime - let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); - let function = function.clone(); - let server_context = server_context.clone(); let query = req .uri() .query() @@ -332,7 +342,22 @@ impl ServerFnHandler { .as_bytes() .to_vec() .into(); + let body = hyper::body::to_bytes(req.body_mut().unwrap()).await; + let Ok(body)=body else { + handle_error(body.err().unwrap(), res); + return; + }; + let headers = req.headers(); + let accept_header = headers.get("Accept").cloned(); + + let parts = Arc::new(extract_parts(req)); + + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); spawn_blocking({ + let function = function.clone(); + let mut server_context = server_context.clone(); + server_context.parts = parts; move || { tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") @@ -349,10 +374,13 @@ impl ServerFnHandler { }); let result = resp_rx.await.unwrap(); + // Set the headers from the server context + *res.headers_mut() = server_context.take_responce_headers(); + match result { Ok(serialized) => { // if this is Accept: application/json then send a serialized JSON response - let accept_header = headers.get("Accept").and_then(|value| value.to_str().ok()); + let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok()); if accept_header == Some("application/json") || accept_header == Some( diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 414160045..31d4daba4 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -51,13 +51,16 @@ use crate::{ prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry, }; +use dioxus_core::VirtualDom; use server_fn::{Encoding, Payload, ServerFunctionRegistry}; use std::error::Error; +use std::sync::Arc; use tokio::task::spawn_blocking; +use warp::path::FullPath; use warp::{ filters::BoxedFilter, http::{Response, StatusCode}, - hyper::{body::Bytes, HeaderMap}, + hyper::body::Bytes, path, Filter, Reply, }; @@ -125,48 +128,16 @@ where /// ``` pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> { register_server_fns_with_handler(server_fn_route, |full_route, func| { - let func2 = func.clone(); - let func3 = func.clone(); path(full_route.clone()) - .and(warp::filters::method::get()) - .and(warp::header::headers_cloned()) - .and(warp::filters::query::raw()) + .and(warp::post().or(warp::get()).unify()) + .and(request_parts()) .and(warp::body::bytes()) - .and_then(move |headers, query, body| { + .and_then(move |parts, bytes| { let func = func.clone(); async move { - server_fn_handler( - DioxusServerContext::default(), - func, - headers, - Some(query), - body, - ) - .await + server_fn_handler(DioxusServerContext::default(), func, parts, bytes).await } }) - .or(path(full_route.clone()) - .and(warp::filters::method::get()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(move |headers, body| { - let func = func2.clone(); - async move { - server_fn_handler(DioxusServerContext::default(), func, headers, None, body) - .await - } - })) - .or(path(full_route) - .and(warp::filters::method::post()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(move |headers, body| { - let func = func3.clone(); - async move { - server_fn_handler(DioxusServerContext::default(), func, headers, None, body) - .await - } - })) }) } @@ -199,18 +170,71 @@ pub fn serve_dioxus_application( + cfg: ServeConfig

, +) -> impl Filter + Clone { + warp::get() + .and(request_parts()) + .and(with_ssr_state()) + .map(move |parts, renderer: SSRState| { + let parts = Arc::new(parts); + + let server_context = DioxusServerContext::new(parts); + + let mut vdom = VirtualDom::new_with_props(cfg.app, cfg.props.clone()) + .with_root_context(server_context.clone()); + let _ = vdom.rebuild(); + + let html = renderer.render_vdom(&vdom, &cfg); + + let mut res = Response::builder(); + + *res.headers_mut().expect("empty request should be valid") = + server_context.take_responce_headers(); + + res.header("Content-Type", "text/html") + .body(Bytes::from(html)) + .unwrap() + }) +} + +/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request. +pub fn request_parts( +) -> impl Filter + Clone { + warp::method() + .and(warp::filters::path::full()) + .and( + warp::filters::query::raw() + .or(warp::any().map(String::new)) + .unify(), + ) + .and(warp::header::headers_cloned()) + .and_then(move |method, path: FullPath, query, headers| async move { + http::uri::Builder::new() + .path_and_query(format!("{}?{}", path.as_str(), query)) + .build() + .map_err(|err| { + warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err))) + }) + .map(|uri| RequestParts { + method, + uri, + headers, + ..Default::default() + }) + }) +} + fn with_ssr_state() -> impl Filter + Clone { - let renderer = SSRState::default(); - warp::any().map(move || renderer.clone()) + let state = SSRState::default(); + warp::any().map(move || state.clone()) } #[derive(Debug)] @@ -227,29 +251,46 @@ impl warp::reject::Reject for RecieveFailed {} pub async fn server_fn_handler( server_context: impl Into, function: ServerFunction, - headers: HeaderMap, - query: Option, + parts: RequestParts, body: Bytes, ) -> Result, warp::Rejection> { - let server_context = server_context.into(); + let mut server_context = server_context.into(); + + let parts = Arc::new(parts); + + server_context.parts = parts.clone(); + // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); spawn_blocking({ move || { tokio::runtime::Runtime::new() .expect("couldn't spawn runtime") - .block_on(async { - let query = &query.unwrap_or_default().into(); + .block_on(async move { + let query = parts + .uri + .query() + .unwrap_or_default() + .as_bytes() + .to_vec() + .into(); let data = match &function.encoding { Encoding::Url | Encoding::Cbor => &body, - Encoding::GetJSON | Encoding::GetCBOR => query, + Encoding::GetJSON | Encoding::GetCBOR => &query, }; - let resp = match (function.trait_obj)(server_context, &data).await { + let resp = match (function.trait_obj)(server_context.clone(), data).await { Ok(serialized) => { // if this is Accept: application/json then send a serialized JSON response - let accept_header = - headers.get("Accept").and_then(|value| value.to_str().ok()); + let accept_header = parts + .headers + .get("Accept") + .as_ref() + .and_then(|value| value.to_str().ok()); let mut res = Response::builder(); + + *res.headers_mut().expect("empty request should be valid") = + server_context.take_responce_headers(); + if accept_header == Some("application/json") || accept_header == Some( diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs index c5ad196bc..8d18dca2a 100644 --- a/packages/server/src/lib.rs +++ b/packages/server/src/lib.rs @@ -30,8 +30,10 @@ pub mod prelude { #[cfg(feature = "ssr")] pub use crate::render::SSRState; #[cfg(feature = "ssr")] - pub use crate::serve_config::{ServeConfig, ServeConfigBuilder}; - pub use crate::server_context::DioxusServerContext; + pub use crate::serve_config::{ ServeConfig, ServeConfigBuilder}; + #[cfg(feature = "ssr")] + pub use crate::server_context::{RequestParts}; + pub use crate::server_context::{ DioxusServerContext, HasServerContext}; pub use crate::server_fn::ServerFn; #[cfg(feature = "ssr")] pub use crate::server_fn::{ServerFnTraitObj, ServerFunction}; diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 0f61a3a0f..e8f6a67ec 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -26,20 +26,31 @@ impl SSRState { /// Render the application to HTML. pub fn render(&self, cfg: &ServeConfig

) -> String { let ServeConfig { - app, props, index, .. + app, props, .. } = cfg; let mut vdom = VirtualDom::new_with_props(*app, props.clone()); let _ = vdom.rebuild(); + self.render_vdom(&vdom, cfg) + } + + /// Render a VirtualDom to HTML. + pub fn render_vdom( + &self, + vdom: &VirtualDom, + cfg: &ServeConfig

, + ) -> String { + let ServeConfig { index, .. } = cfg; + let mut renderer = self.renderers.pull(pre_renderer); let mut html = String::new(); html += &index.pre_main; - let _ = renderer.render_to(&mut html, &vdom); + let _ = renderer.render_to(&mut html, vdom); // serialize the props let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); diff --git a/packages/server/src/server_context.rs b/packages/server/src/server_context.rs index 7e2a494c5..aff9ede35 100644 --- a/packages/server/src/server_context.rs +++ b/packages/server/src/server_context.rs @@ -1,72 +1,160 @@ -use std::sync::{Arc, PoisonError, RwLock, RwLockWriteGuard}; +use dioxus_core::ScopeState; -use anymap::{any::Any, Map}; +/// A trait for an object that contains a server context +pub trait HasServerContext { + /// Get the server context from the state + fn server_context(&self) -> DioxusServerContext; -type SendSyncAnyMap = Map; - -/// A shared context for server functions. -/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions. -#[derive(Clone)] -pub struct DioxusServerContext { - shared_context: Arc>, + /// A shortcut for `self.server_context()` + fn sc(&self) -> DioxusServerContext { + self.server_context() + } } -impl Default for DioxusServerContext { - fn default() -> Self { - Self { - shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())), +impl HasServerContext for &ScopeState { + fn server_context(&self) -> DioxusServerContext { + #[cfg(feature = "ssr")] + { + self.consume_context().expect("No server context found") + } + #[cfg(not(feature = "ssr"))] + { + DioxusServerContext {} } } } -impl DioxusServerContext { - /// Clone a value from the shared server context - pub fn get(&self) -> Option { - self.shared_context.read().ok()?.get::().cloned() - } +/// A shared context for server functions that contains infomation about the request and middleware state. +/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions. +/// +/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope. +#[derive(Clone)] +pub struct DioxusServerContext { + #[cfg(feature = "ssr")] + shared_context: std::sync::Arc< + std::sync::RwLock>, + >, + #[cfg(feature = "ssr")] + headers: std::sync::Arc>, + #[cfg(feature = "ssr")] + pub(crate) parts: std::sync::Arc, +} - /// Insert a value into the shared server context - pub fn insert( - &mut self, - value: T, - ) -> Result<(), PoisonError>> { - self.shared_context - .write() - .map(|mut map| map.insert(value)) - .map(|_| ()) +#[allow(clippy::derivable_impls)] +impl Default for DioxusServerContext { + fn default() -> Self { + Self { + #[cfg(feature = "ssr")] + shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())), + #[cfg(feature = "ssr")] + headers: Default::default(), + #[cfg(feature = "ssr")] + parts: Default::default(), + } } } -/// Generate a server context from a tuple of values -macro_rules! server_context { - ($({$(($name:ident: $ty:ident)),*}),*) => { - $( - #[allow(unused_mut)] - impl< $($ty: Send + Sync + 'static),* > From<($($ty,)*)> for $crate::server_context::DioxusServerContext { - fn from(( $($name,)* ): ($($ty,)*)) -> Self { - let mut context = $crate::server_context::DioxusServerContext::default(); - $(context.insert::<$ty>($name).unwrap();)* - context - } +#[cfg(feature = "ssr")] +pub use server_fn_impl::*; + +#[cfg(feature = "ssr")] +mod server_fn_impl { + use super::*; + use std::sync::LockResult; + use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; + + use anymap::{any::Any, Map}; + type SendSyncAnyMap = Map; + + impl DioxusServerContext { + /// Create a new server context from a request + pub fn new(parts: impl Into>) -> Self { + Self { + parts: parts.into(), + shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())), + headers: Default::default(), } - )* - }; -} + } -server_context!( - {}, - {(a: A)}, - {(a: A), (b: B)}, - {(a: A), (b: B), (c: C)}, - {(a: A), (b: B), (c: C), (d: D)}, - {(a: A), (b: B), (c: C), (d: D), (e: E)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L), (m: M)}, - {(a: A), (b: B), (c: C), (d: D), (e: E), (f: F), (g: G), (h: H), (i: I), (j: J), (k: K), (l: L), (m: M), (n: N)} -); + /// Clone a value from the shared server context + pub fn get(&self) -> Option { + self.shared_context.read().ok()?.get::().cloned() + } + + /// Insert a value into the shared server context + pub fn insert( + &mut self, + value: T, + ) -> Result<(), PoisonError>> { + self.shared_context + .write() + .map(|mut map| map.insert(value)) + .map(|_| ()) + } + + /// Get the headers from the server context + pub fn responce_headers(&self) -> RwLockReadGuard<'_, hyper::header::HeaderMap> { + self.try_responce_headers() + .expect("Failed to get headers from server context") + } + + /// Try to get the headers from the server context + pub fn try_responce_headers( + &self, + ) -> LockResult> { + self.headers.read() + } + + /// Get the headers mutably from the server context + pub fn responce_headers_mut(&self) -> RwLockWriteGuard<'_, hyper::header::HeaderMap> { + self.try_responce_headers_mut() + .expect("Failed to get headers mutably from server context") + } + + /// Try to get the headers mut from the server context + pub fn try_responce_headers_mut( + &self, + ) -> LockResult> { + self.headers.write() + } + + pub(crate) fn take_responce_headers(&self) -> hyper::header::HeaderMap { + let mut headers = self.headers.write().unwrap(); + std::mem::take(&mut *headers) + } + + /// Get the request that triggered: + /// - The initial SSR render if called from a ScopeState or ServerFn + /// - The server function to be called if called from a server function after the initial render + pub fn request_parts(&self) -> &RequestParts { + &self.parts + } + } + + /// Associated parts of an HTTP Request + #[derive(Debug, Default)] + pub struct RequestParts { + /// The request's method + pub method: http::Method, + /// The request's URI + pub uri: http::Uri, + /// The request's version + pub version: http::Version, + /// The request's headers + pub headers: http::HeaderMap, + /// The request's extensions + pub extensions: http::Extensions, + } + + impl From for RequestParts { + fn from(parts: http::request::Parts) -> Self { + Self { + method: parts.method, + uri: parts.uri, + version: parts.version, + headers: parts.headers, + extensions: parts.extensions, + } + } + } +} From 64c7fda201fa718ca2d80c83243d6b537a8e8b53 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 29 Apr 2023 17:32:16 -0500 Subject: [PATCH 50/87] fix warp release builds --- packages/server/src/adapters/warp_adapter.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 31d4daba4..5e9d23562 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -360,12 +360,10 @@ pub fn connect_hot_reload() -> impl Filter Date: Sat, 29 Apr 2023 20:42:47 -0500 Subject: [PATCH 51/87] Remove console.log --- packages/interpreter/src/sledgehammer_bindings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index a15d7bc81..2cb148e89 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -118,7 +118,6 @@ mod js { nodes[id] = node; } export function get_node(id) { - console.log(nodes, id); return nodes[id]; } export function initilize(root, handler) { From 35dcacb9562569434cc5e6f95cefbb95904d26da Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 30 Apr 2023 16:35:11 -0500 Subject: [PATCH 52/87] use ascii64 encoding --- packages/server/Cargo.toml | 1 + .../src/props_html/deserialize_props.rs | 13 ++------ packages/server/src/props_html/mod.rs | 31 ------------------- .../server/src/props_html/serialize_props.rs | 12 ++----- 4 files changed, 7 insertions(+), 50 deletions(-) diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml index b6293993c..753d4c2cb 100644 --- a/packages/server/Cargo.toml +++ b/packages/server/Cargo.toml @@ -47,6 +47,7 @@ tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } futures-util = { version = "0.3.28", optional = true } postcard = { version = "1.0.4", features = ["use-std"] } yazi = "0.1.5" +base64 = "0.21.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { path = "../hot-reload" } diff --git a/packages/server/src/props_html/deserialize_props.rs b/packages/server/src/props_html/deserialize_props.rs index 64f2181ab..dcaa19905 100644 --- a/packages/server/src/props_html/deserialize_props.rs +++ b/packages/server/src/props_html/deserialize_props.rs @@ -1,18 +1,11 @@ use serde::de::DeserializeOwned; -use super::u16_from_char; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; #[allow(unused)] pub(crate) fn serde_from_string(string: &str) -> Option { - let decompressed = string - .chars() - .flat_map(|c| { - let u = u16_from_char(c); - let u1 = (u >> 8) as u8; - let u2 = (u & 0xFF) as u8; - [u1, u2].into_iter() - }) - .collect::>(); + let decompressed = STANDARD.decode(string.as_bytes()).ok()?; let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap(); postcard::from_bytes(&decompressed).ok() diff --git a/packages/server/src/props_html/mod.rs b/packages/server/src/props_html/mod.rs index 9e9b8721e..048e2aa90 100644 --- a/packages/server/src/props_html/mod.rs +++ b/packages/server/src/props_html/mod.rs @@ -52,34 +52,3 @@ fn serialized_and_deserializes() { } } } - -#[test] -fn encodes_and_decodes_bytes() { - for i in 0..(u16::MAX) { - let c = u16_to_char(i); - let i2 = u16_from_char(c); - assert_eq!(i, i2); - } -} - -#[allow(unused)] -pub(crate) fn u16_to_char(u: u16) -> char { - let u = u as u32; - let mapped = if u <= 0xD7FF { - u - } else { - 0xE000 + (u - 0xD7FF) - }; - char::from_u32(mapped).unwrap() -} - -#[allow(unused)] -pub(crate) fn u16_from_char(c: char) -> u16 { - let c = c as u32; - let mapped = if c <= 0xD7FF { - c - } else { - 0xD7FF + (c - 0xE000) - }; - mapped as u16 -} diff --git a/packages/server/src/props_html/serialize_props.rs b/packages/server/src/props_html/serialize_props.rs index f1282f07f..0012afb9c 100644 --- a/packages/server/src/props_html/serialize_props.rs +++ b/packages/server/src/props_html/serialize_props.rs @@ -1,6 +1,7 @@ use serde::Serialize; -use super::u16_to_char; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; #[allow(unused)] pub(crate) fn serde_to_writable( @@ -14,14 +15,7 @@ pub(crate) fn serde_to_writable( yazi::CompressionLevel::BestSize, ) .unwrap(); - for array in compressed.chunks(2) { - let w = if array.len() == 2 { - [array[0], array[1]] - } else { - [array[0], 0] - }; - write_to.write_char(u16_to_char((w[0] as u16) << 8 | (w[1] as u16)))?; - } + write_to.write_str(&STANDARD.encode(compressed)); Ok(()) } From 970d9937580e4c2f2e2c1d5ac982442714da17c9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 1 May 2023 11:27:19 -0500 Subject: [PATCH 53/87] attempt to reconnect to server after sever disconnects in debug mode --- .../server/examples/axum-router/src/main.rs | 3 +- packages/server/src/adapters/axum_adapter.rs | 43 +++++++++++++++---- packages/server/src/adapters/salvo_adapter.rs | 25 ++++++++++- packages/server/src/adapters/warp_adapter.rs | 33 +++++++++++--- packages/server/src/render.rs | 39 +++++++++++++++-- 5 files changed, 122 insertions(+), 21 deletions(-) diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs index 2afa4a481..3994eeb51 100644 --- a/packages/server/examples/axum-router/src/main.rs +++ b/packages/server/examples/axum-router/src/main.rs @@ -9,6 +9,7 @@ use dioxus::prelude::*; use dioxus_router::*; use dioxus_server::prelude::*; +use serde::{Deserialize, Serialize}; fn main() { #[cfg(feature = "web")] @@ -62,7 +63,7 @@ fn main() { } } -#[derive(Clone, Debug, Props, PartialEq)] +#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)] struct AppProps { route: Option, } diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 1b572c804..541094674 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -267,6 +267,7 @@ where .collect::>() .join("/"); let route = format!("/{}", route); + println!("Serving static asset at {}", route); if path.is_dir() { self = self.nest_service(&route, ServeDir::new(path)); } else { @@ -275,20 +276,43 @@ where } // Add server functions and render index.html - self.connect_hot_reload() - .register_server_fns(server_fn_route) - .route( - "/", - get(render_handler).with_state((cfg, SSRState::default())), - ) + self.route( + "/", + get(render_handler).with_state((cfg, SSRState::default())), + ) + .connect_hot_reload() + .register_server_fns(server_fn_route) } fn connect_hot_reload(self) -> Self { #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] { - self.route( - "/_dioxus/hot_reload", - get(hot_reload_handler).with_state(crate::hot_reload::HotReloadState::default()), + self.nest( + "/_dioxus", + Router::new() + .route( + "/hot_reload", + get(hot_reload_handler) + .with_state(crate::hot_reload::HotReloadState::default()), + ) + .route( + "/disconnect", + get(|ws: WebSocketUpgrade| async { + ws.on_failed_upgrade(|error| { + println!("failed to upgrade: {}", error); + todo!() + }) + .on_upgrade(|mut ws| async move { + use axum::extract::ws::Message; + let _ = ws.send(Message::Text("connected".into())).await; + loop { + if ws.recv().await.is_none() { + break; + } + } + }) + }), + ), ) } #[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))] @@ -302,6 +326,7 @@ async fn render_handler( State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, request: Request, ) -> impl IntoResponse { + println!("Rendering"); let (parts, _) = request.into_parts(); let parts: Arc = Arc::new(parts.into()); let server_context = DioxusServerContext::new(parts); diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 6ed4c2b3e..3751211e4 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -261,7 +261,14 @@ impl DioxusRouterExt for Router { } fn connect_hot_reload(self) -> Self { - self.push(Router::with_path("/_dioxus/hot_reload").get(HotReloadHandler::default())) + let mut _dioxus_router = Router::with_path("_dioxus"); + _dioxus_router = _dioxus_router + .push(Router::with_path("hot_reload").handle(HotReloadHandler::default())); + #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] + { + _dioxus_router = _dioxus_router.push(Router::with_path("disconnect").handle(ignore_ws)); + } + self.push(_dioxus_router) } } @@ -504,3 +511,19 @@ impl HotReloadHandler { .await } } + +#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] +#[handler] +async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> { + use salvo::ws::WebSocketUpgrade; + WebSocketUpgrade::new() + .upgrade(req, res, |mut ws| async move { + let _ = dbg!(ws.send(salvo::ws::Message::text("connected")).await); + while let Some(msg) = ws.recv().await { + if msg.is_err() { + return; + }; + } + }) + .await +} diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 5e9d23562..3d41d76cf 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -360,7 +360,7 @@ pub fn connect_hot_reload() -> impl Filter impl Filter impl Filter); + impl Drop for DisconnectOnDrop { + fn drop(&mut self) { + let _ = self.0.take().unwrap().close(); + } + } + + let _ = websocket.send(Message::text("connected")).await; + let mut ws = DisconnectOnDrop(Some(websocket)); + + loop { + ws.0.as_mut().unwrap().next().await; + } + }) + }, + )) } } diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index e8f6a67ec..1d32e2acd 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -25,9 +25,7 @@ impl Default for SSRState { impl SSRState { /// Render the application to HTML. pub fn render(&self, cfg: &ServeConfig

) -> String { - let ServeConfig { - app, props, .. - } = cfg; + let ServeConfig { app, props, .. } = cfg; let mut vdom = VirtualDom::new_with_props(*app, props.clone()); @@ -55,6 +53,41 @@ impl SSRState { // serialize the props let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html); + #[cfg(all(debug_assertions, feature = "hot-reload"))] + { + let disconnect_js = r#"(function () { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = protocol + '//' + window.location.host + '/_dioxus/disconnect'; + const poll_interval = 1000; + const reload_upon_connect = () => { + console.log('Disconnected from server. Attempting to reconnect...'); + window.setTimeout( + () => { + // Try to reconnect to the websocket + const ws = new WebSocket(url); + ws.onopen = () => { + // If we reconnect, reload the page + window.location.reload(); + } + // Otherwise, try again in a second + reload_upon_connect(); + }, + poll_interval); + }; + + // on initial page load connect to the disconnect ws + const ws = new WebSocket(url); + ws.onopen = () => { + // if we disconnect, start polling + ws.onclose = reload_upon_connect; + }; +})()"#; + + html += r#""#; + } + html += &index.post_main; html From 126ed2f9b8cc44fecf9516cd4525f6a744ecfc57 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 1 May 2023 11:28:20 -0500 Subject: [PATCH 54/87] remove log --- packages/server/src/adapters/axum_adapter.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index 541094674..f4b05e199 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -326,7 +326,6 @@ async fn render_handler( State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, request: Request, ) -> impl IntoResponse { - println!("Rendering"); let (parts, _) = request.into_parts(); let parts: Arc = Arc::new(parts.into()); let server_context = DioxusServerContext::new(parts); From 064bee4a9ff3d481622e56ed1e6da5a391020fa7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 1 May 2023 17:34:30 -0500 Subject: [PATCH 55/87] spawn hot reloading in a seperate thread --- .../examples/axum-hello-world/Cargo.toml | 8 +----- .../examples/axum-hello-world/src/main.rs | 15 ++++++++++- .../server/examples/axum-router/Cargo.toml | 8 +----- .../server/examples/axum-router/src/main.rs | 13 +++++++++ .../examples/salvo-hello-world/Cargo.toml | 8 +----- .../examples/salvo-hello-world/src/main.rs | 13 +++++++++ .../examples/warp-hello-world/Cargo.toml | 8 +----- .../examples/warp-hello-world/src/main.rs | 15 ++++++++++- packages/server/src/adapters/axum_adapter.rs | 18 +++++-------- packages/server/src/adapters/salvo_adapter.rs | 15 +++++------ packages/server/src/adapters/warp_adapter.rs | 27 ++++++++++--------- packages/server/src/hot_reload.rs | 15 +++++++++++ packages/server/src/render.rs | 8 +++--- 13 files changed, 106 insertions(+), 65 deletions(-) diff --git a/packages/server/examples/axum-hello-world/Cargo.toml b/packages/server/examples/axum-hello-world/Cargo.toml index b2c14561d..27c6af08b 100644 --- a/packages/server/examples/axum-hello-world/Cargo.toml +++ b/packages/server/examples/axum-hello-world/Cargo.toml @@ -13,15 +13,9 @@ dioxus-server = { path = "../../" } axum = { version = "0.6.12", optional = true } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" +execute = "0.2.12" [features] default = ["web"] ssr = ["axum", "tokio", "dioxus-server/axum"] web = ["dioxus-web"] - -[profile.release] -lto = true -panic = "abort" -opt-level = 3 -strip = true -codegen-units = 1 diff --git a/packages/server/examples/axum-hello-world/src/main.rs b/packages/server/examples/axum-hello-world/src/main.rs index 6b6f32db7..3db117c02 100644 --- a/packages/server/examples/axum-hello-world/src/main.rs +++ b/packages/server/examples/axum-hello-world/src/main.rs @@ -19,6 +19,19 @@ fn main() { ); #[cfg(feature = "ssr")] { + // Start hot reloading + hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| { + execute::shell("dioxus build --features web") + .spawn() + .unwrap() + .wait() + .unwrap(); + execute::shell("cargo run --features ssr --no-default-features") + .spawn() + .unwrap(); + true + })); + PostServerData::register().unwrap(); GetServerData::register().unwrap(); tokio::runtime::Runtime::new() @@ -65,7 +78,7 @@ fn app(cx: Scope) -> Element { } } }, - "Run a server function" + "Run a server function! testing1234" } "Server said: {text}" }) diff --git a/packages/server/examples/axum-router/Cargo.toml b/packages/server/examples/axum-router/Cargo.toml index 3e137d8f6..2ba621c9c 100644 --- a/packages/server/examples/axum-router/Cargo.toml +++ b/packages/server/examples/axum-router/Cargo.toml @@ -16,15 +16,9 @@ tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" tower-http = { version = "0.4.0", features = ["fs"], optional = true } http = { version = "0.2.9", optional = true } +execute = "0.2.12" [features] default = ["web"] ssr = ["axum", "tokio", "dioxus-server/axum", "tower-http", "http"] web = ["dioxus-web", "dioxus-router/web"] - -[profile.release] -lto = true -panic = "abort" -opt-level = 3 -strip = true -codegen-units = 1 diff --git a/packages/server/examples/axum-router/src/main.rs b/packages/server/examples/axum-router/src/main.rs index 3994eeb51..0b5e55e4f 100644 --- a/packages/server/examples/axum-router/src/main.rs +++ b/packages/server/examples/axum-router/src/main.rs @@ -20,6 +20,19 @@ fn main() { ); #[cfg(feature = "ssr")] { + // Start hot reloading + hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| { + execute::shell("dioxus build --features web") + .spawn() + .unwrap() + .wait() + .unwrap(); + execute::shell("cargo run --features ssr --no-default-features") + .spawn() + .unwrap(); + true + })); + use axum::extract::State; PostServerData::register().unwrap(); GetServerData::register().unwrap(); diff --git a/packages/server/examples/salvo-hello-world/Cargo.toml b/packages/server/examples/salvo-hello-world/Cargo.toml index 9146ce3e2..50c92f237 100644 --- a/packages/server/examples/salvo-hello-world/Cargo.toml +++ b/packages/server/examples/salvo-hello-world/Cargo.toml @@ -13,15 +13,9 @@ dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" salvo = { version = "0.37.9", optional = true } +execute = "0.2.12" [features] default = ["web"] ssr = ["salvo", "tokio", "dioxus-server/salvo"] web = ["dioxus-web"] - -[profile.release] -lto = true -panic = "abort" -opt-level = 3 -strip = true -codegen-units = 1 diff --git a/packages/server/examples/salvo-hello-world/src/main.rs b/packages/server/examples/salvo-hello-world/src/main.rs index ff1081077..25ebbbff9 100644 --- a/packages/server/examples/salvo-hello-world/src/main.rs +++ b/packages/server/examples/salvo-hello-world/src/main.rs @@ -19,6 +19,19 @@ fn main() { ); #[cfg(feature = "ssr")] { + // Start hot reloading + hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| { + execute::shell("dioxus build --features web") + .spawn() + .unwrap() + .wait() + .unwrap(); + execute::shell("cargo run --features ssr --no-default-features") + .spawn() + .unwrap(); + true + })); + use salvo::prelude::*; PostServerData::register().unwrap(); GetServerData::register().unwrap(); diff --git a/packages/server/examples/warp-hello-world/Cargo.toml b/packages/server/examples/warp-hello-world/Cargo.toml index e182494b0..2c5d175ae 100644 --- a/packages/server/examples/warp-hello-world/Cargo.toml +++ b/packages/server/examples/warp-hello-world/Cargo.toml @@ -13,15 +13,9 @@ dioxus-server = { path = "../../" } tokio = { version = "1.27.0", features = ["full"], optional = true } serde = "1.0.159" warp = { version = "0.3.3", optional = true } +execute = "0.2.12" [features] default = ["web"] ssr = ["warp", "tokio", "dioxus-server/warp"] web = ["dioxus-web"] - -[profile.release] -lto = true -panic = "abort" -opt-level = 3 -strip = true -codegen-units = 1 diff --git a/packages/server/examples/warp-hello-world/src/main.rs b/packages/server/examples/warp-hello-world/src/main.rs index 04684f803..3bbb95227 100644 --- a/packages/server/examples/warp-hello-world/src/main.rs +++ b/packages/server/examples/warp-hello-world/src/main.rs @@ -19,6 +19,19 @@ fn main() { ); #[cfg(feature = "ssr")] { + // Start hot reloading + hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| { + execute::shell("dioxus build --features web") + .spawn() + .unwrap() + .wait() + .unwrap(); + execute::shell("cargo run --features ssr --no-default-features") + .spawn() + .unwrap(); + true + })); + PostServerData::register().unwrap(); GetServerData::register().unwrap(); tokio::runtime::Runtime::new() @@ -45,7 +58,7 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count += 10, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } button { onclick: move |_| { diff --git a/packages/server/src/adapters/axum_adapter.rs b/packages/server/src/adapters/axum_adapter.rs index f4b05e199..6ca82b1e5 100644 --- a/packages/server/src/adapters/axum_adapter.rs +++ b/packages/server/src/adapters/axum_adapter.rs @@ -290,11 +290,7 @@ where self.nest( "/_dioxus", Router::new() - .route( - "/hot_reload", - get(hot_reload_handler) - .with_state(crate::hot_reload::HotReloadState::default()), - ) + .route("/hot_reload", get(hot_reload_handler)) .route( "/disconnect", get(|ws: WebSocketUpgrade| async { @@ -420,14 +416,13 @@ fn report_err(e: E) -> Response { /// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] -pub async fn hot_reload_handler( - ws: WebSocketUpgrade, - State(state): State, -) -> impl IntoResponse { +pub async fn hot_reload_handler(ws: WebSocketUpgrade) -> impl IntoResponse { use axum::extract::ws::Message; use futures_util::StreamExt; - ws.on_upgrade(|mut socket| async move { + let state = crate::hot_reload::spawn_hot_reload().await; + + ws.on_upgrade(move |mut socket| async move { println!("🔥 Hot Reload WebSocket connected"); { // update any rsx calls that changed before the websocket connected. @@ -448,7 +443,8 @@ pub async fn hot_reload_handler( println!("finished"); } - let mut rx = tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver); + let mut rx = + tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver.clone()); while let Some(change) = rx.next().await { if let Some(template) = change { let template = { serde_json::to_string(&template).unwrap() }; diff --git a/packages/server/src/adapters/salvo_adapter.rs b/packages/server/src/adapters/salvo_adapter.rs index 3751211e4..75387d1f9 100644 --- a/packages/server/src/adapters/salvo_adapter.rs +++ b/packages/server/src/adapters/salvo_adapter.rs @@ -455,9 +455,7 @@ impl HotReloadHandler { /// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change. #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] #[derive(Default)] -pub struct HotReloadHandler { - state: crate::hot_reload::HotReloadState, -} +pub struct HotReloadHandler; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] #[handler] @@ -471,10 +469,10 @@ impl HotReloadHandler { use salvo::ws::Message; use salvo::ws::WebSocketUpgrade; - let state = self.state.clone(); + let state = crate::hot_reload::spawn_hot_reload().await; WebSocketUpgrade::new() - .upgrade(req, res, |mut websocket| async move { + .upgrade(req, res, move |mut websocket| async move { use futures_util::StreamExt; println!("🔥 Hot Reload WebSocket connected"); @@ -497,8 +495,9 @@ impl HotReloadHandler { println!("finished"); } - let mut rx = - tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver); + let mut rx = tokio_stream::wrappers::WatchStream::from_changes( + state.message_receiver.clone(), + ); while let Some(change) = rx.next().await { if let Some(template) = change { let template = { serde_json::to_string(&template).unwrap() }; @@ -518,7 +517,7 @@ async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::h use salvo::ws::WebSocketUpgrade; WebSocketUpgrade::new() .upgrade(req, res, |mut ws| async move { - let _ = dbg!(ws.send(salvo::ws::Message::text("connected")).await); + let _ = ws.send(salvo::ws::Message::text("connected")).await; while let Some(msg) = ws.recv().await { if msg.is_err() { return; diff --git a/packages/server/src/adapters/warp_adapter.rs b/packages/server/src/adapters/warp_adapter.rs index 3d41d76cf..9a4ec7af4 100644 --- a/packages/server/src/adapters/warp_adapter.rs +++ b/packages/server/src/adapters/warp_adapter.rs @@ -372,12 +372,10 @@ pub fn connect_hot_reload() -> impl Filter impl Filter impl Filter); @@ -432,10 +433,12 @@ pub fn connect_hot_reload() -> impl Filter = tokio::sync::OnceCell::const_new(); +pub(crate) async fn spawn_hot_reload() -> &'static HotReloadState { + HOT_RELOAD_STATE + .get_or_init(|| async { + println!("spinning up hot reloading"); + let r = tokio::task::spawn_blocking(HotReloadState::default) + .await + .unwrap(); + println!("hot reloading ready"); + r + }) + .await +} diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index 1d32e2acd..a05a63801 100644 --- a/packages/server/src/render.rs +++ b/packages/server/src/render.rs @@ -55,6 +55,7 @@ impl SSRState { #[cfg(all(debug_assertions, feature = "hot-reload"))] { + // In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work let disconnect_js = r#"(function () { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const url = protocol + '//' + window.location.host + '/_dioxus/disconnect'; @@ -77,10 +78,9 @@ impl SSRState { // on initial page load connect to the disconnect ws const ws = new WebSocket(url); - ws.onopen = () => { - // if we disconnect, start polling - ws.onclose = reload_upon_connect; - }; + // if we disconnect, start polling + ws.onmessage = (m) => {console.log(m)}; + ws.onclose = reload_upon_connect; })()"#; html += r#"