diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/Makefile b/Makefile index 90d29a6..f7d3a7d 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,16 @@ book_pages: demo_todomvc: @echo "====> building demo todomvc" rm -rf gh-pages/demos/todomvc - wasm-pack build --release --target web --out-dir ../../gh-pages/demos/todomvc/pkg examples/web-todomvc + wasm-pack build --release --target web --out-dir ../../gh-pages/demos/todomvc/pkg examples/web-todomvc --features=csr rm gh-pages/demos/todomvc/pkg/.gitignore cp examples/web-todomvc/index.html gh-pages/demos/todomvc/ +.PHONY: demo_todomvc_ssr +demo_todomvc_ssr: + @echo "====> running demo todomvc ssr" + # FIXME: how to unset target set via cargo config and just use current system's? + cargo run -p web-todomvc --features=ssr --bin ssr --target=x86_64-unknown-linux-gnu + .PHONY: demo_simple demo_simple: @echo "====> building demo simple" @@ -40,4 +46,4 @@ demo_login_flow: cp examples/login-flow/index.html gh-pages/demos/login-flow/ .PHONY: demos -demos: demo_todomvc demo_simple demo_x_bow_playground demo_login_flow \ No newline at end of file +demos: demo_todomvc demo_simple demo_x_bow_playground demo_login_flow diff --git a/async_ui_web/Cargo.toml b/async_ui_web/Cargo.toml index c6ce578..51e5c66 100644 --- a/async_ui_web/Cargo.toml +++ b/async_ui_web/Cargo.toml @@ -27,6 +27,8 @@ scopeguard = "1.1.0" js-sys = "0.3.64" wasm-bindgen = "0.2.87" +pin-project = "1.0" + [dependencies.web-sys] version = "0.3.64" features = [ @@ -35,5 +37,11 @@ features = [ 'IntersectionObserver', 'IntersectionObserverInit', 'IntersectionObserverEntry', - 'console' -] \ No newline at end of file + 'console', +] + +[features] +default = [] +csr = ['async_ui_web_core/csr', 'async_ui_web_html/csr'] +ssr = ['async_ui_web_core/ssr', 'async_ui_web_html/ssr'] + diff --git a/async_ui_web/src/executor.rs b/async_ui_web/src/executor.rs index 85ffaac..f1539ee 100644 --- a/async_ui_web/src/executor.rs +++ b/async_ui_web/src/executor.rs @@ -7,6 +7,7 @@ use std::{cell::OnceCell, future::pending}; use async_executor::LocalExecutor; use async_ui_web_core::executor::set_executor_future; +use std::future::Future; thread_local! { static EXECUTOR: OnceCell<&'static LocalExecutor<'static>> = OnceCell::new(); @@ -32,3 +33,12 @@ pub fn get_executor() -> &'static LocalExecutor<'static> { }) }) } + +/// If something needs to be run to completion before sending user html - it needs to be wrapped in `run_loading` call. +/// +/// let data = run_loading(load_data()).await; +/// +/// DataDisplay::new(data).render().await +pub async fn run_loading(f: impl Future) -> V { + f.await +} diff --git a/async_ui_web/src/executor_ssr.rs b/async_ui_web/src/executor_ssr.rs new file mode 100644 index 0000000..a2f4f73 --- /dev/null +++ b/async_ui_web/src/executor_ssr.rs @@ -0,0 +1,91 @@ +use std::cell::RefCell; +use std::future::Future; +use std::pin::{pin, Pin}; +use std::task::{Context, Poll}; + +pub fn get_executor() -> &'static () { + todo!("tokio/other executor abstraction is wanted here.") +} + +/// If something needs to be run to completion before sending user html - it needs to be wrapped in `run_loading` call. +/// +/// let data = run_loading(load_data()).await; +/// +/// DataDisplay::new(data).render().await +/// +/// Future will be pooled until all run_loading futures are resolved +// TODO: We also want user to be able to re-use loaded data during hydration, make an additional wrapper function, which will also feed frontend with serialized data version? +pub async fn run_loading(f: impl Future) -> V { + CTX.with_borrow_mut(|ctx| { + let ctx = ctx + .as_mut() + .expect("ctx should be set by UntilLoadedFuture (before)"); + ctx.loading += 1; + }); + let res = f.await; + CTX.with_borrow_mut(|ctx| { + let ctx = ctx + .as_mut() + .expect("ctx should be set by UntilLoadedFuture (after)"); + ctx.loading -= 1; + }); + res +} + +struct SsrContext { + loading: usize, +} + +thread_local! { + static CTX: RefCell> = const { RefCell::new(None) }; +} + +#[pin_project::pin_project] +struct UntilLoadedFuture { + #[pin] + inner: F, + ctx: Option, + ran_once: bool, +} +impl> Future for UntilLoadedFuture { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let project = self.project(); + let ctx = project + .ctx + .take() + .expect("ctx is returned in between polls"); + CTX.with_borrow_mut(move |v| { + assert!(v.is_none(), "CTX is empty between polls"); + *v = Some(ctx); + }); + + let poll = project.inner.poll(cx); + + let ctx = CTX.with_borrow_mut(move |v| v.take().expect("nothing should retake our ctx")); + + if ctx.loading == 0 { + // We don't care about everything not needed for first contentful load. + return Poll::Ready(()); + } + + // TODO: This is not panic-safe, but I'm not sure how panics can be handled here yet. + *project.ctx = Some(ctx); + *project.ran_once = true; + + match poll { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } +} + +pub(crate) async fn poll_until_loaded(inner: impl Future) { + let fut = UntilLoadedFuture { + inner, + ctx: Some(SsrContext { loading: 0 }), + ran_once: false, + }; + fut.await +} diff --git a/async_ui_web/src/lib.rs b/async_ui_web/src/lib.rs index ad67eb4..36e9f71 100644 --- a/async_ui_web/src/lib.rs +++ b/async_ui_web/src/lib.rs @@ -6,18 +6,28 @@ See the [guide book](https://wishawa.github.io/async_ui/book/) to get started! */ pub mod components; +#[cfg(feature = "csr")] +pub mod executor; +#[cfg(feature = "ssr")] +#[path = "./executor_ssr.rs"] pub mod executor; pub mod lists; +#[cfg(feature = "csr")] mod mount; mod no_child; mod shortcuts; +#[cfg(feature = "ssr")] +mod ssr; pub use async_ui_internal_utils::reactive_cell::ReactiveCell; pub use async_ui_web_core::combinators::{join, race, race_ok, try_join}; pub use async_ui_web_html::nodes as html; pub use async_ui_web_macros::css; pub use async_ui_web_macros::select; +#[cfg(feature = "csr")] pub use mount::{mount, mount_at}; +#[cfg(feature = "ssr")] +pub use ssr::render_to_string; pub use no_child::NoChild; #[doc(hidden)] diff --git a/async_ui_web/src/lists/dynamic_list.rs b/async_ui_web/src/lists/dynamic_list.rs index b51ec0c..e362c13 100644 --- a/async_ui_web/src/lists/dynamic_list.rs +++ b/async_ui_web/src/lists/dynamic_list.rs @@ -8,13 +8,15 @@ use std::{ }; use async_executor::{LocalExecutor, Task}; -use async_ui_web_core::{ContainerNodeFuture, DetachmentBlocker, SiblingNodeFuture}; +use async_ui_web_core::{ + dom::{DocumentFragment, Node, marker_node}, + ContainerNodeFuture, DetachmentBlocker, SiblingNodeFuture, +}; use wasm_bindgen::UnwrapThrowExt; -use web_sys::{Comment, DocumentFragment}; #[derive(Clone, Debug)] enum ContainingNode { - Real(web_sys::Node), + Real(Node), Fake(DocumentFragment), } @@ -53,8 +55,8 @@ join(( pub struct DynamicList<'c, K: Eq + Hash, F: Future + 'c> { inner: RefCell>, executor: LocalExecutor<'c>, - list_end_marker: web_sys::Node, - list_start_marker: web_sys::Node, + list_end_marker: Node, + list_start_marker: Node, detachment_blocker: DetachmentBlocker, } @@ -65,12 +67,12 @@ struct DynamicListInner { struct Stored { task: Task, - start_marker: web_sys::Node, - end_marker: web_sys::Node, + start_marker: Node, + end_marker: Node, } impl ContainingNode { - fn get(&self) -> &web_sys::Node { + fn get(&self) -> &Node { match self { ContainingNode::Real(real) => real, ContainingNode::Fake(fake) => fake, @@ -87,8 +89,8 @@ impl<'c, K: Eq + Hash, F: Future + 'c> DynamicList<'c, K, F> { /// Create a new list, without anything in it. pub fn new() -> Self { let frag = DocumentFragment::new().unwrap_throw(); - let list_end_marker = Comment::new().unwrap_throw().into(); - let list_start_marker = Comment::new().unwrap_throw().into(); + let list_end_marker = marker_node("list end").into(); + let list_start_marker = marker_node("list start").into(); frag.append_child(&list_start_marker).unwrap_throw(); frag.append_child(&list_end_marker).unwrap_throw(); Self { @@ -122,8 +124,8 @@ impl<'c, K: Eq + Hash, F: Future + 'c> DynamicList<'c, K, F> { pub fn insert(&self, key: K, future: F, before: Option<&K>) -> bool { let mut inner = self.inner.borrow_mut(); let container = inner.containing_node.get(); - let start_marker: web_sys::Node = Comment::new().unwrap_throw().into(); - let end_marker: web_sys::Node = Comment::new().unwrap_throw().into(); + let start_marker: Node = marker_node("item start").into(); + let end_marker: Node = marker_node("item end").into(); let after = before .map(|k| &inner.items.get(k).unwrap().start_marker) .unwrap_or(&self.list_end_marker); @@ -349,10 +351,10 @@ impl<'c, K: Eq + Hash, F: Future> Drop for DynamicList<'c, K, F> { /// Move `start_marker`, `end_marker`, and eveything between them /// into `container` at location before `after`. fn move_nodes_before( - container: &web_sys::Node, - start_marker: &web_sys::Node, - end_marker: &web_sys::Node, - after: Option<&web_sys::Node>, + container: &Node, + start_marker: &Node, + end_marker: &Node, + after: Option<&Node>, ) { let mut node = start_marker.clone(); loop { diff --git a/async_ui_web/src/lists/mod.rs b/async_ui_web/src/lists/mod.rs index 63234a4..3e23de2 100644 --- a/async_ui_web/src/lists/mod.rs +++ b/async_ui_web/src/lists/mod.rs @@ -58,9 +58,12 @@ mod diffed_list; mod dynamic_list; mod modeled_list; +// TODO: Somehow make its api SSR-friendly? +#[cfg(feature = "csr")] mod virtualized_list; pub use diffed_list::DiffedList; pub use dynamic_list::DynamicList; pub use modeled_list::{ListModel, ModeledList}; +#[cfg(feature = "csr")] pub use virtualized_list::VirtualizedList; diff --git a/async_ui_web/src/lists/virtualized_list.rs b/async_ui_web/src/lists/virtualized_list.rs index 507a6f1..0604eea 100644 --- a/async_ui_web/src/lists/virtualized_list.rs +++ b/async_ui_web/src/lists/virtualized_list.rs @@ -7,10 +7,14 @@ use std::{ }; use async_ui_internal_utils::reactive_cell::ReactiveCell; -use async_ui_web_core::{combinators::join, ContainerNodeFuture}; +use async_ui_web_core::{ + combinators::join, + dom::{Element, HtmlElement}, + ContainerNodeFuture, +}; use futures_lite::{Future, StreamExt}; use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt}; -use web_sys::{Element, HtmlElement, IntersectionObserver, IntersectionObserverInit}; +use web_sys::{IntersectionObserver, IntersectionObserverInit}; use super::DynamicList; diff --git a/async_ui_web/src/shortcuts.rs b/async_ui_web/src/shortcuts.rs index 053940d..bac541d 100644 --- a/async_ui_web/src/shortcuts.rs +++ b/async_ui_web/src/shortcuts.rs @@ -1,9 +1,9 @@ use std::future::Pending; +use async_ui_web_core::dom::Element; use async_ui_web_core::ContainerNodeFuture; use async_ui_web_html::nodes::Text; -use js_sys::Array; -use wasm_bindgen::{JsValue, UnwrapThrowExt}; +use wasm_bindgen::UnwrapThrowExt; pub trait ShortcutRenderStr { /// Render the [str] as an HTML text node with that content. @@ -45,28 +45,42 @@ pub trait ShortcutClassList { } /// Convert an iterator of str to a JS array of strings. -fn strs_to_js_array<'a>(values: impl Iterator) -> Array { - values.into_iter().map(JsValue::from_str).collect() +#[cfg(feature = "csr")] +fn strs_to_js_array<'a>(values: impl Iterator) -> js_sys::Array { + values + .into_iter() + .map(wasm_bindgen::JsValue::from_str) + .collect() } -impl ShortcutClassList for web_sys::Element { +impl ShortcutClassList for Element { fn add_class(&self, c: &str) { self.class_list().add_1(c).unwrap_throw(); } + #[cfg(feature = "csr")] fn add_classes<'a>(&self, c: impl IntoIterator) { self.class_list() .add(&strs_to_js_array(c.into_iter())) .unwrap_throw(); } + #[cfg(not(feature = "csr"))] + fn add_classes<'a>(&self, c: impl IntoIterator) { + self.class_list().add(c).unwrap_throw(); + } fn del_class(&self, c: &str) { self.class_list().remove_1(c).unwrap(); } + #[cfg(feature = "csr")] fn del_classes<'a>(&self, c: impl IntoIterator) { self.class_list() .remove(&strs_to_js_array(c.into_iter())) .unwrap_throw(); } + #[cfg(not(feature = "csr"))] + fn del_classes<'a>(&self, c: impl IntoIterator) { + self.class_list().remove(c).unwrap_throw(); + } fn set_class(&self, c: &str, included: bool) { self.class_list() @@ -75,7 +89,7 @@ impl ShortcutClassList for web_sys::Element { } } -pub trait ShortcutClassListBuilder: AsRef { +pub trait ShortcutClassListBuilder: AsRef { /// Add a classname to the element and return reference to the input. /// /// This is for writing the UI "declaratively". @@ -105,4 +119,4 @@ pub trait ShortcutClassListBuilder: AsRef { self } } -impl> ShortcutClassListBuilder for T {} +impl> ShortcutClassListBuilder for T {} diff --git a/async_ui_web/src/ssr.rs b/async_ui_web/src/ssr.rs new file mode 100644 index 0000000..2f780da --- /dev/null +++ b/async_ui_web/src/ssr.rs @@ -0,0 +1,26 @@ +use std::future::Future; +use std::pin::pin; +use std::task::{ready, Poll}; + +use async_ui_web_core::dom::{create_ssr_element, SsrNode}; +use futures_lite::future::poll_fn; + +use crate::executor; + +pub async fn render_to_string(child_future: F) -> String { + let node = AsRef::::as_ref(&create_ssr_element("#root")).clone(); + let mut root_fut = + pin!(async_ui_web_core::ContainerNodeFuture::new_root(child_future, node.clone())); + executor::poll_until_loaded(async { + poll_fn(|cx| { + ready!(root_fut.as_mut().poll(cx)); + Poll::Ready(Some(())) + }) + .await; + unreachable!("no pending render futures, an empty app?\nassuming something is broken"); + }) + .await; + // inner to strip outer <#root> + let out = node.to_inner_html(); + out +} diff --git a/async_ui_web_core/Cargo.toml b/async_ui_web_core/Cargo.toml index 9ba1ed9..175dcdc 100644 --- a/async_ui_web_core/Cargo.toml +++ b/async_ui_web_core/Cargo.toml @@ -21,12 +21,14 @@ async_ui_internal_utils = { version = "0.0.2", path = "../async_ui_internal_util [dependencies.web-sys] version = "0.3.64" -features = [ - 'console', - 'Node', - 'Window', - 'Document', -] +optional = true +features = ['console', 'Node', 'Window', 'Document'] [dev-dependencies] -futures-lite = "1.13.0" \ No newline at end of file +futures-lite = "1.13.0" + +[features] +default = [] +csr = ["web-sys"] +ssr = [] +hydrate = [] diff --git a/async_ui_web_core/src/combinators/common/tuple.rs b/async_ui_web_core/src/combinators/common/tuple.rs index 3360979..77a54cc 100644 --- a/async_ui_web_core/src/combinators/common/tuple.rs +++ b/async_ui_web_core/src/combinators/common/tuple.rs @@ -230,7 +230,11 @@ macro_rules! impl_common_tuple { fn drop(self: Pin<&mut Self>) { let this = self.project(); - if !this.detachment_blocker.block_until_drop() { + + if !this.detachment_blocker.block_until_drop() + // There is some bug here, in SSR rendering it fails during drop + // due to missing context, I think it is broken for top-level elements? + && (cfg!(feature = "csr") || DOM_CONTEXT.is_set()) { DOM_CONTEXT.with(|parent: &DomContext| parent.remove_child(ChildPosition::default())); } diff --git a/async_ui_web_core/src/context.rs b/async_ui_web_core/src/context.rs index 5e0b277..5239a5f 100644 --- a/async_ui_web_core/src/context.rs +++ b/async_ui_web_core/src/context.rs @@ -2,17 +2,18 @@ use std::{cell::RefCell, collections::BTreeMap}; use wasm_bindgen::UnwrapThrowExt; +use crate::dom::Node; use crate::position::ChildPosition; pub(crate) enum DomContext<'p> { Container { group: &'p NodeGroup, - container: &'p web_sys::Node, + container: &'p Node, }, Sibling { parent: &'p Self, group: &'p NodeGroup, - reference: &'p web_sys::Node, + reference: &'p Node, }, Child { parent: &'p Self, @@ -26,12 +27,12 @@ scoped_tls_hkt::scoped_thread_local!( pub(crate) static DOM_CONTEXT: for<'p> &'p DomContext<'p> ); -pub(crate) type NodeGroup = RefCell>; +pub(crate) type NodeGroup = RefCell>; impl<'p> DomContext<'p> { /// Get the HTML node where the current code would render in. /// This is used by [SiblingNodeFuture][crate::SiblingNodeFuture] to decide where to add children. - pub fn get_containing_node(&self) -> &web_sys::Node { + pub fn get_containing_node(&self) -> &Node { match self { DomContext::Container { container, .. } => container, DomContext::Child { parent, .. } | DomContext::Sibling { parent, .. } => { @@ -42,7 +43,7 @@ impl<'p> DomContext<'p> { } } /// Add a new node `new_child` ordered relative to existing siblings according to the given [ChildPosition]. - pub fn add_child(&self, mut position: ChildPosition, new_child: web_sys::Node) { + pub fn add_child(&self, mut position: ChildPosition, new_child: Node) { match self { DomContext::Container { group, container } => { let mut group = group.borrow_mut(); @@ -99,9 +100,9 @@ impl<'p> DomContext<'p> { } fn remove_children_here( - tree: &mut BTreeMap, + tree: &mut BTreeMap, position: ChildPosition, - container: &web_sys::Node, + container: &Node, ) { if position.is_root() { tree.values().for_each(|child| { @@ -119,18 +120,23 @@ fn remove_children_here( } #[cfg(debug_assertions)] -fn panic_if_duplicate_node(node: Option) { +fn panic_if_duplicate_node(node: Option) { if let Some(node) = node { - web_sys::console::error_2( - &"Attempted to insert two nodes at the same position.\n\ - You probably either used a `join` implementation from outside Async UI,\ - or tried to render something in a spawned Future.\n\ - This message is only shown in debug builds.\n\ - Check the code where you render this node:\ - " - .into(), - node.as_ref(), - ); + #[cfg(feature = "csr")] + { + web_sys::console::error_2( + &"Attempted to insert two nodes at the same position.\n\ + You probably either used a `join` implementation from outside Async UI,\ + or tried to render something in a spawned Future.\n\ + This message is only shown in debug builds.\n\ + Check the code where you render this node:\ + " + .into(), + node.as_ref(), + ); + } + drop(node); + // TODO: Message in SSR panic!() } } diff --git a/async_ui_web_core/src/dom.rs b/async_ui_web_core/src/dom.rs new file mode 100644 index 0000000..b4a41c0 --- /dev/null +++ b/async_ui_web_core/src/dom.rs @@ -0,0 +1,22 @@ +use wasm_bindgen::UnwrapThrowExt; + +pub type Node = web_sys::Node; +pub type Element = web_sys::Element; +pub type HtmlElement = web_sys::HtmlElement; +pub type Text = web_sys::Text; +pub type EventTarget = web_sys::EventTarget; +pub type DocumentFragment = web_sys::DocumentFragment; +pub type Comment = web_sys::Comment; + +pub use web_sys as elements; + +#[inline] +pub fn marker_node(dbg: &'static str) -> Comment { + + let c = Comment::new().unwrap_throw(); + #[cfg(debug_assertions)] + { + c.set_data(dbg); + } + c +} diff --git a/async_ui_web_core/src/dom_ssr.rs b/async_ui_web_core/src/dom_ssr.rs new file mode 100644 index 0000000..a95c4b0 --- /dev/null +++ b/async_ui_web_core/src/dom_ssr.rs @@ -0,0 +1,916 @@ +use std::cell::{Ref, RefCell, RefMut}; +use std::collections::HashSet; +use std::fmt::{self, Display}; +use std::mem; +use std::ops::Deref; +use std::rc::{Rc, Weak}; +pub type Node = SsrNode; +pub type Element = SsrElement; +pub type HtmlElement = SsrHtmlElement; +pub type Text = SsrText; +pub type EventTarget = SsrEventTarget; +pub type DocumentFragment = SsrDocumentFragment; +pub type Comment = SsrComment; + +pub struct SsrEventTarget {} + +impl SsrEventTarget { + pub fn event_subscribed(&self, _name: &str) { + // I think it might be good to I.e disable checkboxes etc + // in SSR when there is an event subscription created, making it impossible + // to interact with such elements until client hydration is complete? + // println!("event subscribed: {name}") + } + pub fn event_unsubscribed(&self, _name: &str) { + // println!("event unsubscribed: {name}") + } + pub fn to_owned(&self) -> Self { + Self {} + } +} + +pub mod elements { + use super::{Element, SsrHtmlElement, HtmlElement, Node}; + use std::ops::Deref; + + macro_rules! impl_element { + ($name:ident) => { + #[repr(transparent)] + pub struct $name(HtmlElement); + + impl AsRef for $name { + fn as_ref(&self) -> &Node { + &self.0 .0 + } + } + impl AsRef for $name { + fn as_ref(&self) -> &Element { + &self.0 .0 + } + } + impl AsRef for $name { + fn as_ref(&self) -> &HtmlElement { + &self.0 + } + } + impl AsRef<$name> for $name { + fn as_ref(&self) -> &Self { + self + } + } + impl Deref for $name { + type Target = HtmlElement; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } + } + impl TryFrom for $name { + type Error = (); + + fn try_from(value: Element) -> Result { + // Nothing is done for non-html elements yet, all elements are html elements + Ok(Self(SsrHtmlElement(value))) + } + } + }; + } + impl_element!(HtmlAnchorElement); + impl_element!(HtmlAreaElement); + impl_element!(HtmlAudioElement); + // impl_element!(HtmlBElement); + impl_element!(HtmlBrElement); + impl_element!(HtmlBaseElement); + impl_element!(HtmlButtonElement); + impl_element!(HtmlCanvasElement); + impl_element!(HtmlDListElement); + impl_element!(HtmlDataElement); + impl_element!(HtmlDataListElement); + impl_element!(HtmlDialogElement); + impl_element!(HtmlDivElement); + impl_element!(HtmlEmbedElement); + impl_element!(HtmlFieldSetElement); + impl_element!(HtmlFormElement); + impl_element!(HtmlFrameSetElement); + impl_element!(HtmlHrElement); + impl_element!(HtmlHeadingElement); + impl_element!(HtmlImageElement); + impl_element!(HtmlIFrameElement); + impl_element!(HtmlInputElement); + impl_element!(HtmlLiElement); + impl_element!(HtmlLabelElement); + impl_element!(HtmlLegendElement); + impl_element!(HtmlLinkElement); + impl_element!(HtmlMapElement); + impl_element!(HtmlMetaElement); + impl_element!(HtmlMeterElement); + impl_element!(HtmlOListElement); + impl_element!(HtmlObjectElement); + impl_element!(HtmlOptGroupElement); + impl_element!(HtmlOptionElement); + impl_element!(HtmlOutputElement); + impl_element!(HtmlParagraphElement); + impl_element!(HtmlPictureElement); + impl_element!(HtmlPreElement); + impl_element!(HtmlProgressElement); + impl_element!(HtmlQuoteElement); + impl_element!(HtmlSelectElement); + impl_element!(HtmlSourceElement); + impl_element!(HtmlSpanElement); + impl_element!(HtmlStyleElement); + impl_element!(HtmlTableCellElement); + impl_element!(HtmlTableColElement); + impl_element!(HtmlTableElement); + impl_element!(HtmlTableRowElement); + impl_element!(HtmlTableSectionElement); + impl_element!(HtmlTemplateElement); + impl_element!(HtmlTextAreaElement); + impl_element!(HtmlTimeElement); + impl_element!(HtmlTrackElement); + impl_element!(HtmlUListElement); + impl_element!(HtmlVideoElement); + + macro_rules! ats { + ($id:ident, &str, attr: $v:literal) => { + pub fn $id(&self, v: &str) { + self.set_attribute($v, v) + } + }; + ($id:ident, bool, attr: $v:literal) => { + pub fn $id(&self, v: bool) { + if v { + self.set_attribute($v, "") + } else { + self.remove_attribute($v); + } + } + }; + ($id:ident, f64, attr: $v:literal) => { + pub fn $id(&self, v: f64) { + self.set_attribute($v, &v.to_string()) + } + }; + ($id:ident, &str, noop) => { + pub fn $id(&self, _v: &str) {} + }; + ($id:ident, bool, noop) => { + pub fn $id(&self, _v: bool) {} + }; + } + macro_rules! atg { + ($id:ident, $ty:ty, $v:expr) => { + pub fn $id(&self) -> $ty { + $v + } + }; + // Some values can be get back from the attribute + ($id:ident, $ty:ty, attr: $name:literal $(.map(|$mi:ident| $me:expr))? = $v:expr) => { + pub fn $id(&self) -> $ty { + self.get_attribute($name) + $(.map(|$mi| $me))? + .unwrap_or_else(|| $v) + } + }; + } + impl HtmlElement { + ats!(set_hidden, bool, attr: "hidden"); + ats!(set_title, &str, attr: "title"); + } + impl HtmlButtonElement { + ats!(set_disabled, bool, attr: "disabled"); + } + impl HtmlLabelElement { + ats!(set_html_for, &str, attr: "for"); + } + impl HtmlInputElement { + ats!(set_type, &str, attr: "type"); + ats!(set_placeholder, &str, attr: "placeholder"); + ats!(set_autofocus, bool, attr: "autofocus"); + ats!(set_value, &str, attr: "value"); + atg!(value, String, attr: "value" = "".to_owned()); + atg!(value_as_number, f64, attr: "value".map(|v| v.parse().unwrap_or(f64::NAN)) = f64::NAN); + atg!(checked, bool, attr: "checked".map(|_v| true) = true); + ats!(set_checked, bool, attr: "checked"); + ats!(set_step, &str, attr: "step"); + ats!(set_min, &str, attr: "min"); + ats!(set_max, &str, attr: "max"); + ats!(set_disabled, bool, attr: "disabled"); + } + impl HtmlOptionElement { + // TODO: Other sibling `HtmlSelectElement` options should be reset here, should it be emulated? + // Can't do that server-side. Without that, server may produce invalid html. + // + // Maybe some generic on-load-prop-setting option should be added for not available attributes?.. + // E.g data-oncreate="self.selected = true"... It won't work with noscript, however. + ats!(set_selected, bool, attr: "selected"); + atg!(value, String, attr: "value" = "".to_owned()); + ats!(set_value, &str, attr: "value"); + ats!(set_disabled, bool, attr: "disabled"); + } + impl HtmlSelectElement { + ats!(set_placeholder, &str, attr: "placeholder"); + // Is a property, not an attribute, see comment on set_selected in `HtmlOptionElement` + ats!(set_value, &str, noop); + atg!(value, String, "".to_owned()); + // Until set_value is not working, default index is ok... + atg!(selected_index, i32, 0); + ats!(set_autofocus, bool, attr: "autofocus"); + ats!(set_multiple, bool, attr: "multiple"); + ats!(set_disabled, bool, attr: "disabled"); + } + impl HtmlAnchorElement { + ats!(set_href, &str, attr: "href"); + } + impl HtmlMeterElement { + ats!(set_min, f64, attr: "min"); + ats!(set_max, f64, attr: "max"); + ats!(set_value, f64, attr: "value"); + } +} + +#[derive(Debug, Clone)] +pub struct SsrStyle(SsrElement); +impl SsrStyle { + pub fn set_property(&self, property: &str, value: &str) -> Option<()> { + let mut element = self.0.borrow_element_mut(); + if value.is_empty() { + if let Some(pos) = element.style.iter().position(|(k, _)| k == property) { + element.style.remove(pos); + } + } else if let Some((_, v)) = element.style.iter_mut().find(|(k, _)| k == property) { + *v = value.to_owned() + } else { + element.style.push((property.to_owned(), value.to_owned())); + } + Some(()) + } +} + +// It is possible to make it just a wrapper that calls `set_attribute(class, ...)`... +// Except async-ui already provides api for classList manipulation, and we need to optimize for this case +#[derive(Debug, Clone)] +pub struct SsrClassList(SsrElement); +impl SsrClassList { + pub fn add_1(&self, class: &str) -> Option<()> { + let mut element = self.0.borrow_element_mut(); + + let class = class.to_owned(); + + if !element.classes.contains(&class) { + element.classes.push(class); + } + + Some(()) + } + pub fn remove_1(&self, class: &str) -> Option<()> { + let mut element = self.0.borrow_element_mut(); + + let pos = element.classes.iter().position(|el| el == class); + if let Some(pos) = pos { + element.classes.remove(pos); + } + + Some(()) + } + // TODO: Delegate add_1 to add, not the other way around + pub fn add<'s>(&self, c: impl IntoIterator) -> Option<()> { + for c in c { + self.add_1(c)?; + } + + Some(()) + } + // TODO: Delegate add_1 to add, not the other way around + pub fn remove<'s>(&self, c: impl IntoIterator) -> Option<()> { + for c in c { + self.remove_1(c)?; + } + + Some(()) + } + pub fn toggle_with_force(&self, c: &str, force: bool) -> Option<()> { + if force { + self.add_1(c) + } else { + self.remove_1(c) + } + } +} + +#[derive(Debug)] +struct SsrElementData { + name: String, + classes: Vec, + style: Vec<(String, String)>, + attrs: Vec<(String, String)>, + children: Vec, +} + +#[derive(Debug)] +enum SsrNodeKind { + Text(String), + Element(SsrElementData), + DocumentFragment { children: Vec }, + Comment(String), +} +#[derive(Debug)] +struct SsrNodeInner { + kind: SsrNodeKind, + parent: Option, +} + +#[derive(Debug)] +struct WeakSsrNode(Weak>); +impl WeakSsrNode { + fn upgrade(&self) -> Option { + self.0.upgrade().map(SsrNode) + } +} + +fn check_attr_name(name: &str) { + if name == "class" || name == "style" { + // Should be possible for `Element`, but not for `HtmlElement`. + // For `HtmlElement` those accesses should be rewritten to use style declaration or classlist. + panic!("unable to alter class/style attributes"); + } +} + +#[derive(Clone, Debug)] +pub struct SsrElement(SsrNode); +impl SsrElement { + fn borrow_element(&self) -> Ref<'_, SsrElementData> { + Ref::map(self.0 .0.borrow(), |node| match &node.kind { + SsrNodeKind::Element(ssr_element_data) => ssr_element_data, + SsrNodeKind::Text(_) + | SsrNodeKind::DocumentFragment { .. } + | SsrNodeKind::Comment(_) => { + unreachable!("invalid SsrElement: should be of Element node kind") + } + }) + } + fn borrow_element_mut(&self) -> RefMut<'_, SsrElementData> { + RefMut::map(self.0 .0.borrow_mut(), |node| match &mut node.kind { + SsrNodeKind::Element(ssr_element_data) => ssr_element_data, + SsrNodeKind::Text(_) + | SsrNodeKind::DocumentFragment { .. } + | SsrNodeKind::Comment(_) => { + unreachable!("invalid SsrElement: should be of Element node kind") + } + }) + } + + pub fn class_list(&self) -> SsrClassList { + SsrClassList(self.clone()) + } + pub fn get_attribute(&self, name: &str) -> Option { + check_attr_name(name); + let element = self.borrow_element(); + + let (_, v) = element.attrs.iter().find(|(n, _)| n == name)?; + Some(v.to_owned()) + } + pub fn set_attribute(&self, name: &str, value: &str) { + check_attr_name(name); + let mut element = self.borrow_element_mut(); + + if let Some((_, v)) = element.attrs.iter_mut().find(|(n, _)| n == name) { + *v = value.to_owned(); + } else { + element.attrs.push((name.to_string(), value.to_string())); + } + } + pub fn remove_attribute(&self, name: &str) { + check_attr_name(name); + let mut element = self.borrow_element_mut(); + + if let Some(pos) = element.attrs.iter_mut().position(|(n, _)| n == name) { + element.attrs.remove(pos); + } + } + // TODO: Should conflicts be handled? + pub fn set_id(&self, id: &str) { + self.set_attribute("id", id); + } +} +impl AsRef for SsrElement { + fn as_ref(&self) -> &SsrNode { + &self.0 + } +} +impl AsRef for SsrElement { + fn as_ref(&self) -> &SsrElement { + self + } +} +impl From for SsrNode { + fn from(value: SsrElement) -> Self { + value.0 + } +} +impl Deref for SsrElement { + type Target = Node; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct SsrHtmlElement(SsrElement); +impl SsrHtmlElement { + pub fn style(&self) -> SsrStyle { + SsrStyle(self.0.clone()) + } + pub fn set_inner_text(&self, text: &str) { + self.set_text_content(Some(text)); + } +} +impl AsRef for SsrHtmlElement { + fn as_ref(&self) -> &SsrNode { + &self.0 .0 + } +} +impl AsRef for SsrHtmlElement { + fn as_ref(&self) -> &SsrElement { + &self.0 + } +} +impl AsRef for SsrHtmlElement { + fn as_ref(&self) -> &SsrHtmlElement { + self + } +} +impl Deref for SsrHtmlElement { + type Target = SsrElement; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} +impl TryFrom for SsrHtmlElement { + type Error = (); + + fn try_from(value: SsrElement) -> Result { + // Nothing is done for non-html elements yet, all elements are html elements + Ok(Self(value)) + } +} + +#[derive(Clone)] +pub struct SsrText(SsrNode); +impl SsrText { + pub fn set_data(&self, text: &str) { + self.0.set_text_content(Some(text)); + } +} +impl AsRef for SsrText { + fn as_ref(&self) -> &SsrNode { + &self.0 + } +} +impl From for SsrNode { + fn from(value: SsrText) -> Self { + value.0 + } +} +#[derive(Clone, Debug)] +pub struct SsrDocumentFragment(SsrNode); +impl SsrDocumentFragment { + pub fn new() -> Option { + Some(Self(SsrNode(Rc::new(RefCell::new(SsrNodeInner { + kind: SsrNodeKind::DocumentFragment { children: vec![] }, + parent: None, + }))))) + } +} +impl AsRef for SsrDocumentFragment { + fn as_ref(&self) -> &SsrNode { + &self.0 + } +} +impl From for SsrNode { + fn from(value: SsrDocumentFragment) -> Self { + value.0 + } +} +impl Deref for SsrDocumentFragment { + type Target = Node; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +#[derive(Clone)] +pub struct SsrComment(SsrNode); +impl SsrComment { + pub fn new() -> Option { + Some(Self(SsrNode(Rc::new(RefCell::new(SsrNodeInner { + kind: SsrNodeKind::Comment("".to_owned()), + parent: None, + }))))) + } + pub fn set_data(&self, text: &str) { + self.0.set_text_content(Some(text)); + } +} +impl AsRef for SsrComment { + fn as_ref(&self) -> &SsrNode { + &self.0 + } +} +impl From for SsrNode { + fn from(value: SsrComment) -> Self { + value.0 + } +} +impl Deref for SsrComment { + type Target = Node; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// If something is not used in SSR rendering, return this to avoid warnings caused +// by usage of empty tuple. +pub struct Unused; + +#[derive(Clone, Debug)] +pub struct SsrNode(Rc>); +impl SsrNode { + fn take_parent(&self) { + // borrow_mut to immediately take `node.parent` `Option`, might not be required + // but doing that to be safe. + let node = self.0.borrow(); + if let Some(parent) = &node.parent { + // unreachable!("experiment: check if reparenting happens this way in async-ui"); + if let Some(parent) = parent.upgrade() { + drop(node); + parent + .remove_child(self) + .expect("parent is set for node => parent should contain this child"); + } + } + } + // fn take_parent_for_remarent(&self, new_parent: &Self) { + // // borrow_mut to immediately take `node.parent` `Option`, might not be required + // // but doing that to be safe. + // let mut node = self.0.borrow_mut(); + // if let Some(parent) = node.parent.take() { + // unreachable!("experiment: check if reparenting happens this way in async-ui"); + // if let Some(parent) = parent.upgrade() { + // drop(node); + // parent.remove_child(&parent); + // } + // } + // } + fn take_known_parent(&self, known: &Self) { + let mut node = self.0.borrow_mut(); + let parent = node.parent.take().expect("parent doesn't exists"); + { + let parent = parent.upgrade().expect("parent should live at this point"); + drop(node); + assert!( + Self::ptr_eq(&parent, known), + "parent is either not set or known" + ); + } + } + + pub fn focus(&self) -> Option<()> { + // Noop, should it somehow be communicated to CSR/hydration, + // e.g by anchor url or startup script? + Some(()) + } + pub fn parent_node(&self) -> Option { + let node = self.0.borrow(); + let parent = node.parent.as_ref()?; + let v = parent + .upgrade() + .expect("parent shouldn't be dropped that early"); + Some(v) + } + pub fn next_sibling(&self) -> Option { + let v = self.parent_node()?; + let node = v.0.borrow(); + let (SsrNodeKind::Element(SsrElementData { children, .. }) + | SsrNodeKind::DocumentFragment { children }) = &node.kind + else { + unreachable!("parent might only be element or document fragment"); + }; + let pos = children + .iter() + .position(|el| Self::ptr_eq(el, self)) + .expect("parent should contain child"); + let sibling = children.get(pos + 1)?.clone(); + assert!(!sibling.is_same_node(Some(self))); + Some(sibling) + } + pub fn is_same_node(&self, other: Option<&Node>) -> bool { + let Some(other) = other else { + return false; + }; + Self::ptr_eq(self, other) + } + pub fn append_child(&self, new_node: &Node) -> Option<()> { + self.insert_before(new_node, None) + } + pub fn insert_before(&self, new_node: &Node, reference_node: Option<&Node>) -> Option<()> { + assert!( + !new_node.is_same_node(Some(self)), + "the new child can't be a parent" + ); + // insert_before removes node from the previous parent first. + // Not sure it if matters in async-ui, but matching DOM behavior first. + new_node.take_parent(); + + let mut node = self.0.borrow_mut(); + match &mut node.kind { + SsrNodeKind::Text(_) | SsrNodeKind::Comment(_) => { + // TODO: Error: Cannot add children to a Text + None + } + SsrNodeKind::Element(SsrElementData { children, .. }) + | SsrNodeKind::DocumentFragment { children } => { + // Find the insert position + let mut pos = if let Some(reference_node) = reference_node { + // TODO: Error: Child to insert before is not a child of this node + let pos = children + .iter() + .position(|el| Self::ptr_eq(el, reference_node))?; + Some(pos) + } else { + None + }; + + let mut node = new_node.0.borrow_mut(); + if let SsrNodeKind::DocumentFragment { + children: frag_child, + } = &mut node.kind + { + for child in std::mem::take(frag_child) { + let mut child_node = child.0.borrow_mut(); + child_node.parent = Some(Self::downgrade(self)); + drop(child_node); + + if let Some(pos) = &mut pos { + children.insert(*pos, child.clone()); + *pos += 1; + } else { + children.push(child.clone()); + } + } + } else { + // Update node parent + node.parent = Some(Self::downgrade(self)); + drop(node); + + // Perform insertion + if let Some(pos) = pos { + children.insert(pos, new_node.clone()); + } else { + children.push(new_node.clone()); + } + } + Some(()) + } + } + } + + /// None corresponds to web NotFoundError: the node to be removed is not a child of this node. + // TODO: Return removed child? + pub fn remove_child(&self, child: &Node) -> Option { + assert!(!self.is_same_node(Some(child)), "parent != child"); + let mut node = self.0.borrow_mut(); + match &mut node.kind { + SsrNodeKind::Text(_) | SsrNodeKind::Comment(_) => None, + SsrNodeKind::Element(SsrElementData { children, .. }) + | SsrNodeKind::DocumentFragment { children } => { + let pos = children.iter().position(|el| Self::ptr_eq(el, child))?; + children.remove(pos); + drop(node); + child.take_known_parent(self); + Some(Unused) + } + } + } + + pub fn set_text_content(&self, text: Option<&str>) { + let mut node = self.0.borrow_mut(); + match &mut node.kind { + SsrNodeKind::Text(v) | SsrNodeKind::Comment(v) => { + *v = text.unwrap_or_default().to_owned(); + } + SsrNodeKind::Element(SsrElementData { children, .. }) + | SsrNodeKind::DocumentFragment { children } => { + let old_children = mem::take(children); + children.push(create_ssr_text(text.unwrap_or_default()).0); + drop(node); + + for child in old_children { + child.take_known_parent(self); + } + } + } + } + + fn downgrade(this: &Self) -> WeakSsrNode { + WeakSsrNode(Rc::downgrade(&this.0)) + } + + fn ptr_eq(a: &Self, b: &Self) -> bool { + Rc::ptr_eq(&a.0, &b.0) + } + + pub fn to_html(&self) -> String { + self.to_html_impl(false) + } + pub fn to_inner_html(&self) -> String { + self.to_html_impl(true) + } + + fn to_html_impl(&self, mut inner: bool) -> String { + let mut out = String::new(); + self.serialize_html(&mut inner, &mut HashSet::new(), &mut false, &mut out) + .expect("fmt shouldn't fail"); + out + } + + fn serialize_html( + &self, + skip_this: &mut bool, + visited: &mut HashSet, + last_is_text: &mut bool, + out: &mut String, + ) -> fmt::Result { + use std::fmt::Write; + + struct Text<'s>(&'s str); + impl Display for Text<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for ele in self.0.chars() { + match ele { + '&' => write!(f, "&")?, + '<' => write!(f, "<")?, + '>' => write!(f, ">")?, + c => write!(f, "{c}")?, + } + } + Ok(()) + } + } + struct AttrValue<'s>(&'s str); + impl Display for AttrValue<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for ele in self.0.chars() { + match ele { + '&' => write!(f, "&")?, + '<' => write!(f, "<")?, + '>' => write!(f, ">")?, + '"' => write!(f, """)?, + c => write!(f, "{c}")?, + } + } + Ok(()) + } + } + let id_addr = Rc::as_ptr(&self.0).addr(); + #[cfg(debug_assertions)] + if !visited.insert(id_addr) { + write!(out, "")?; + *last_is_text = false; + return Ok(()); + } + + let skipped = *skip_this; + *skip_this = false; + + let node = self.0.borrow(); + match &node.kind { + SsrNodeKind::DocumentFragment { children } => { + write!( + out, + "" + )?; + for child in children { + child.serialize_html(skip_this, visited, last_is_text, out)?; + } + write!(out, "")?; + *last_is_text = false; + } + SsrNodeKind::Text(t) => { + if *last_is_text && cfg!(feature = "hydrate") { + // For hydration - ensure text nodes are separated, as with real DOM building + write!(out, "")?; + } + write!(out, "{}", Text(t))?; + *last_is_text = true; + } + SsrNodeKind::Element(SsrElementData{ + name, + classes, + attrs, + children, + style, + }) => { + // TODO: Ensure there is nothing criminal in element name/attrs? + if !skipped { + out.push('<'); + out.push_str(name); + // #[cfg(debug_assertions)] + // { + // // Debug piece for cycle debugging + // write!(out, " nod=\"{id_addr:x}\"")?; + // } + { + // TODO: Ensure added classes have no spaces in them? + if !classes.is_empty() { + out.push_str(" class=\""); + for (i, ele) in classes.iter().enumerate() { + if i != 0 { + out.push(' '); + } + write!(out, "{}", AttrValue(ele))?; + } + out.push('"'); + } + } + { + // TODO: Properties are usually processed by css parser, it would be pretty costly to do that + // in SSR, implement some cheaper form of verification. + if !style.is_empty() { + out.push_str(" style=\""); + for (k, v) in style.iter() { + write!(out, "{k}: {v};")?; + } + out.push('"'); + } + } + for (k, v) in attrs { + write!(out, " {k}=\"{}\"", AttrValue(v))?; + } + } + if children.is_empty() { + // Closing self-closing element is not valid in HTML4, ensure that DOCTYPE html is passed for html5 compat + if !skipped { + out.push_str("/>"); + } + } else { + if !skipped { + *last_is_text = false; + out.push('>'); + } + for child in children { + child.serialize_html(skip_this, visited, last_is_text, out)?; + } + if !skipped { + write!(out, "")?; + } + } + if !skipped { + *last_is_text = false; + } + } + SsrNodeKind::Comment(c) => { + // Is comment content even important? Maybe for some hydration markers? + // TODO: Make sure nothing is broken due to nod display + // TODO: Ensure proper escaping + write!(out, "")?; + //nod=\"{id_addr:x}\"-->")?; + *last_is_text = false; + } + } + + #[cfg(debug_assertions)] + if !visited.remove(&id_addr) { + panic!("visited marker disappeared"); + } + Ok(()) + } +} + +pub fn create_ssr_element(name: &str) -> SsrElement { + SsrElement(SsrNode(Rc::new(RefCell::new(SsrNodeInner { + kind: SsrNodeKind::Element(SsrElementData { + name: name.to_owned(), + attrs: vec![], + children: vec![], + classes: vec![], + style: vec![], + }), + parent: None, + })))) +} +pub fn create_ssr_text(name: &str) -> SsrText { + SsrText(SsrNode(Rc::new(RefCell::new(SsrNodeInner { + kind: SsrNodeKind::Text(name.to_owned()), + parent: None, + })))) +} + +#[inline] +pub fn marker_node(dbg: &'static str) -> Comment { + let c = Comment::new().expect("marker"); + #[cfg(debug_assertions)] + { + c.set_data(dbg); + } + c +} diff --git a/async_ui_web_core/src/lib.rs b/async_ui_web_core/src/lib.rs index 2997181..1c2e904 100644 --- a/async_ui_web_core/src/lib.rs +++ b/async_ui_web_core/src/lib.rs @@ -1,7 +1,19 @@ pub mod combinators; +#[cfg(feature = "csr")] pub mod executor; +#[cfg(feature = "csr")] pub mod window; +#[cfg(feature = "csr")] +pub mod dom; +// Make lints happy by default, by making ssr items always visible +#[cfg(not(feature = "csr"))] +#[path = "./dom_ssr.rs"] +pub mod dom; + +#[cfg(all(feature = "csr", feature = "ssr"))] +compile_error!("csr and ssr features are mutually exclusive!"); + mod context; mod dropping; mod node_container; diff --git a/async_ui_web_core/src/node_container.rs b/async_ui_web_core/src/node_container.rs index ca5858e..513fb2f 100644 --- a/async_ui_web_core/src/node_container.rs +++ b/async_ui_web_core/src/node_container.rs @@ -8,6 +8,7 @@ use pin_project::{pin_project, pinned_drop}; use crate::{ context::{DomContext, NodeGroup, DOM_CONTEXT}, + dom::Node, dropping::DetachmentBlocker, position::ChildPosition, }; @@ -19,7 +20,7 @@ pub struct ContainerNodeFuture { #[pin] child_future: C, group: NodeGroup, - container: web_sys::Node, + container: Node, add_self: AddSelfMode, drop: DetachmentBlocker, } @@ -35,7 +36,7 @@ impl ContainerNodeFuture { /// Return a future wrapping the given child future. /// Any node rendered by the child future will appear inside the given node. /// Upon first poll of the future `node` will be added to the parent. - pub fn new(child_future: C, node: web_sys::Node) -> Self { + pub fn new(child_future: C, node: Node) -> Self { Self { child_future, group: Default::default(), @@ -45,7 +46,7 @@ impl ContainerNodeFuture { } } /// Like `new` but `node` won't be added to the parent (do that manually). - pub fn new_root(child_future: C, node: web_sys::Node) -> Self { + pub fn new_root(child_future: C, node: Node) -> Self { Self { child_future, group: Default::default(), diff --git a/async_ui_web_core/src/node_sibling.rs b/async_ui_web_core/src/node_sibling.rs index a7ef1ed..4b0a26a 100644 --- a/async_ui_web_core/src/node_sibling.rs +++ b/async_ui_web_core/src/node_sibling.rs @@ -7,9 +7,7 @@ use std::{ use pin_project::{pin_project, pinned_drop}; use crate::{ - context::{DomContext, NodeGroup, DOM_CONTEXT}, - dropping::DetachmentBlocker, - position::ChildPosition, + context::{DomContext, NodeGroup, DOM_CONTEXT}, dom::Node, dropping::DetachmentBlocker, position::ChildPosition }; /// Future wrapper where anything rendered in its child will appear as a sibling of a node. @@ -21,12 +19,12 @@ pub struct SiblingNodeFuture { #[pin] child_future: C, group: NodeGroup, - reference: web_sys::Node, + reference: Node, drop: DetachmentBlocker, } impl SiblingNodeFuture { - pub fn new(child_future: C, sibling: web_sys::Node) -> Self { + pub fn new(child_future: C, sibling: Node) -> Self { Self { child_future, group: Default::default(), diff --git a/async_ui_web_html/Cargo.toml b/async_ui_web_html/Cargo.toml index fcc4f6b..afa4fa1 100644 --- a/async_ui_web_html/Cargo.toml +++ b/async_ui_web_html/Cargo.toml @@ -24,81 +24,6 @@ async-executor = "1.5.0" [dependencies.web-sys] version = "0.3.64" features = [ - 'Node', - 'Window', - 'Document', - 'HtmlElement', - 'HtmlAudioElement', - 'HtmlVideoElement', - 'HtmlCanvasElement', - 'HtmlTableColElement', - 'HtmlDataListElement', - 'HtmlMetaElement', - 'HtmlLabelElement', - 'HtmlHeadElement', - 'HtmlBrElement', - 'HtmlTableSectionElement', - 'HtmlDListElement', - 'HtmlTableElement', - 'HtmlUListElement', - 'HtmlTableCellElement', - 'HtmlDetailsElement', - 'HtmlProgressElement', - 'HtmlSlotElement', - 'HtmlQuoteElement', - 'HtmlTimeElement', - 'HtmlTemplateElement', - 'HtmlTitleElement', - 'HtmlOptGroupElement', - 'HtmlSourceElement', - 'HtmlFontElement', - 'HtmlMeterElement', - 'HtmlFormElement', - 'HtmlIFrameElement', - 'HtmlMediaElement', - 'HtmlLinkElement', - 'HtmlBodyElement', - 'HtmlAreaElement', - 'HtmlHrElement', - 'HtmlMapElement', - 'HtmlEmbedElement', - 'HtmlDataElement', - 'HtmlSpanElement', - 'HtmlLiElement', - 'HtmlOListElement', - 'HtmlAnchorElement', - 'HtmlMenuElement', - 'HtmlHtmlElement', - 'HtmlTableRowElement', - 'HtmlDialogElement', - 'HtmlPreElement', - 'HtmlLegendElement', - 'HtmlFrameElement', - 'HtmlStyleElement', - 'HtmlTableCaptionElement', - 'HtmlOptionElement', - 'HtmlDivElement', - 'HtmlFrameSetElement', - 'HtmlDirectoryElement', - 'HtmlPictureElement', - 'HtmlTextAreaElement', - 'HtmlImageElement', - 'HtmlObjectElement', - 'HtmlUnknownElement', - 'HtmlBaseElement', - 'HtmlInputElement', - 'HtmlScriptElement', - 'HtmlOutputElement', - 'HtmlMenuItemElement', - 'HtmlParamElement', - 'HtmlModElement', - 'HtmlSelectElement', - 'HtmlTrackElement', - 'HtmlButtonElement', - 'HtmlParagraphElement', - 'HtmlHeadingElement', - 'HtmlFieldSetElement', - 'Text', 'ClipboardEvent', 'CompositionEvent', 'FocusEvent', @@ -109,8 +34,94 @@ features = [ 'WheelEvent', 'DragEvent', # 'ClipboardEvent', - 'Comment', - 'DomTokenList', - 'CssStyleDeclaration', - 'AddEventListenerOptions' +] + +[features] +default = [] +# Events are not feature-gated to make code just work somehow without +# shimming them all, event handlers are noop in SSR. +csr = [ + 'async_ui_web_core/csr', + 'web-sys/Node', + 'web-sys/Window', + 'web-sys/Document', + 'web-sys/Text', + 'web-sys/Comment', + 'web-sys/DomTokenList', + 'web-sys/CssStyleDeclaration', + 'web-sys/AddEventListenerOptions', + 'web-sys/HtmlElement', + 'web-sys/HtmlAudioElement', + 'web-sys/HtmlVideoElement', + 'web-sys/HtmlCanvasElement', + 'web-sys/HtmlTableColElement', + 'web-sys/HtmlDataListElement', + 'web-sys/HtmlMetaElement', + 'web-sys/HtmlLabelElement', + 'web-sys/HtmlHeadElement', + 'web-sys/HtmlBrElement', + 'web-sys/HtmlTableSectionElement', + 'web-sys/HtmlDListElement', + 'web-sys/HtmlTableElement', + 'web-sys/HtmlUListElement', + 'web-sys/HtmlTableCellElement', + 'web-sys/HtmlDetailsElement', + 'web-sys/HtmlProgressElement', + 'web-sys/HtmlSlotElement', + 'web-sys/HtmlQuoteElement', + 'web-sys/HtmlTimeElement', + 'web-sys/HtmlTemplateElement', + 'web-sys/HtmlTitleElement', + 'web-sys/HtmlOptGroupElement', + 'web-sys/HtmlSourceElement', + 'web-sys/HtmlFontElement', + 'web-sys/HtmlMeterElement', + 'web-sys/HtmlFormElement', + 'web-sys/HtmlIFrameElement', + 'web-sys/HtmlMediaElement', + 'web-sys/HtmlLinkElement', + 'web-sys/HtmlBodyElement', + 'web-sys/HtmlAreaElement', + 'web-sys/HtmlHrElement', + 'web-sys/HtmlMapElement', + 'web-sys/HtmlEmbedElement', + 'web-sys/HtmlDataElement', + 'web-sys/HtmlSpanElement', + 'web-sys/HtmlLiElement', + 'web-sys/HtmlOListElement', + 'web-sys/HtmlAnchorElement', + 'web-sys/HtmlMenuElement', + 'web-sys/HtmlHtmlElement', + 'web-sys/HtmlTableRowElement', + 'web-sys/HtmlDialogElement', + 'web-sys/HtmlPreElement', + 'web-sys/HtmlLegendElement', + 'web-sys/HtmlFrameElement', + 'web-sys/HtmlStyleElement', + 'web-sys/HtmlTableCaptionElement', + 'web-sys/HtmlOptionElement', + 'web-sys/HtmlDivElement', + 'web-sys/HtmlFrameSetElement', + 'web-sys/HtmlDirectoryElement', + 'web-sys/HtmlPictureElement', + 'web-sys/HtmlTextAreaElement', + 'web-sys/HtmlImageElement', + 'web-sys/HtmlObjectElement', + 'web-sys/HtmlUnknownElement', + 'web-sys/HtmlBaseElement', + 'web-sys/HtmlInputElement', + 'web-sys/HtmlScriptElement', + 'web-sys/HtmlOutputElement', + 'web-sys/HtmlMenuItemElement', + 'web-sys/HtmlParamElement', + 'web-sys/HtmlModElement', + 'web-sys/HtmlSelectElement', + 'web-sys/HtmlTrackElement', + 'web-sys/HtmlButtonElement', + 'web-sys/HtmlParagraphElement', + 'web-sys/HtmlHeadingElement', + 'web-sys/HtmlFieldSetElement', +] +ssr = [ + 'async_ui_web_core/ssr', ] diff --git a/async_ui_web_html/src/common_components.rs b/async_ui_web_html/src/common_components.rs index ec24de2..596de9f 100644 --- a/async_ui_web_html/src/common_components.rs +++ b/async_ui_web_html/src/common_components.rs @@ -4,11 +4,15 @@ use std::{ ops::Deref, }; -use async_ui_web_core::{window::DOCUMENT, ContainerNodeFuture}; +use async_ui_web_core::{ + dom::{self, elements, Node}, + ContainerNodeFuture, +}; +#[cfg(feature = "csr")] use wasm_bindgen::prelude::{JsCast, UnwrapThrowExt}; macro_rules! component_impl { - ($ty:ident, $tag_name:literal, $elem_ty:ty, $link:tt) => { + ($ty:ident, $tag_name:literal, $elem_ty:ident, $link:tt) => { #[doc = "The HTML `"] #[doc = $tag_name] #[doc = "` tag."] @@ -16,7 +20,7 @@ macro_rules! component_impl { #[doc = $link] #[doc = "."] pub struct $ty { - pub element: $elem_ty, + pub element: elements::$elem_ty, } impl $ty { #[doc = "Create a new instance of this type."] @@ -28,6 +32,11 @@ macro_rules! component_impl { element: create_element($tag_name), } } + + #[doc(hidden)] + pub fn render_explicit_children(&self, c: F) -> ContainerNodeFuture { + ContainerNodeFuture::new(c, AsRef::::as_ref(&self.element).clone()) + } } impl Default for $ty { fn default() -> Self { @@ -35,21 +44,21 @@ macro_rules! component_impl { } } impl Deref for $ty { - type Target = $elem_ty; + type Target = elements::$elem_ty; fn deref(&self) -> &Self::Target { &self.element } } impl AsRef for $ty where - $elem_ty: AsRef, + elements::$elem_ty: AsRef, { fn as_ref(&self) -> &X { self.element.as_ref() } } }; - ($ty:ident, $tag_name:literal, $elem_ty:ty, $link:tt, childed) => { + ($ty:ident, $tag_name:literal, $elem_ty:ident, $link:tt, childed) => { component_impl!($ty, $tag_name, $elem_ty, $link); impl $ty { #[doc = "Put this HTML element on the screen."] @@ -61,11 +70,11 @@ macro_rules! component_impl { #[doc = ""] #[doc = "This method should only be called once. It may misbehave otherwise."] pub fn render(&self, c: F) -> ContainerNodeFuture { - ContainerNodeFuture::new(c, AsRef::::as_ref(&self.element).clone()) + self.render_explicit_children(c) } } }; - ($ty:ident, $tag_name:literal, $elem_ty:ty, $link:tt, childless) => { + ($ty:ident, $tag_name:literal, $elem_ty:ident, $link:tt, childless) => { component_impl!($ty, $tag_name, $elem_ty, $link); impl $ty { #[doc = "Put this HTML element on the screen."] @@ -76,10 +85,7 @@ macro_rules! component_impl { #[doc = ""] #[doc = "This method should only be called once. It may misbehave otherwise."] pub fn render(&self) -> ContainerNodeFuture> { - ContainerNodeFuture::new( - pending(), - AsRef::::as_ref(&self.element).clone(), - ) + self.render_explicit_children(pending()) } } }; @@ -88,76 +94,76 @@ macro_rules! component_impl { #[rustfmt::skip] mod impls { use super::*; - component_impl!(Anchor, "a", web_sys::HtmlAnchorElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)", childed); - component_impl!(Area, "area", web_sys::HtmlAreaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)", childless); - component_impl!(Audio, "audio", web_sys::HtmlAudioElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio)", childed); - component_impl!(Bold, "b", web_sys::HtmlBrElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b)", childed); - component_impl!(Br, "br", web_sys::HtmlBrElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br)", childless); - component_impl!(Base, "base", web_sys::HtmlBaseElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)", childless); - component_impl!(Button, "button", web_sys::HtmlButtonElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)", childed); - component_impl!(Canvas, "canvas", web_sys::HtmlCanvasElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)", childed); - component_impl!(Dl, "dl", web_sys::HtmlDListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl)", childed); - component_impl!(Data, "data", web_sys::HtmlDataElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data)", childed); - component_impl!(DataList, "datalist", web_sys::HtmlDataListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)", childed); - component_impl!(Dialog, "dialog", web_sys::HtmlDialogElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)", childed); - component_impl!(Div, "div", web_sys::HtmlDivElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div)", childed); - component_impl!(Embed, "embed", web_sys::HtmlEmbedElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed)", childless); - component_impl!(FieldSet, "fieldset", web_sys::HtmlFieldSetElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)", childed); - component_impl!(Form, "form", web_sys::HtmlFormElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)", childed); - component_impl!(FrameSet, "frameset", web_sys::HtmlFrameSetElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frameset)", childed); - component_impl!(Hr, "hr", web_sys::HtmlHrElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr)", childless); - component_impl!(H1, "h1", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)", childed); - component_impl!(H2, "h2", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2)", childed); - component_impl!(H3, "h3", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3)", childed); - component_impl!(H4, "h4", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4)", childed); - component_impl!(H5, "h5", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5)", childed); - component_impl!(H6, "h6", web_sys::HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6)", childed); - component_impl!(Italic, "i", web_sys::HtmlImageElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i)", childed); - component_impl!(IFrame, "iframe", web_sys::HtmlIFrameElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)", childed); - component_impl!(Img, "img", web_sys::HtmlImageElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img)", childed); - component_impl!(Input, "input", web_sys::HtmlInputElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)", childless); - component_impl!(Li, "li", web_sys::HtmlLiElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)", childed); - component_impl!(Label, "label", web_sys::HtmlLabelElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)", childed); - component_impl!(Legend, "legend", web_sys::HtmlLegendElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)", childed); - component_impl!(Link, "link", web_sys::HtmlLinkElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)", childless); - component_impl!(Map, "map", web_sys::HtmlMapElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map)", childed); - component_impl!(Meta, "meta", web_sys::HtmlMetaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)", childless); - component_impl!(Meter, "meter", web_sys::HtmlMeterElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter)", childed); - component_impl!(Ol, "ol", web_sys::HtmlOListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol)", childed); - component_impl!(Object, "object", web_sys::HtmlObjectElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)", childed); - component_impl!(OptGroup, "optgroup", web_sys::HtmlOptGroupElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup)", childed); - component_impl!(Option, "option", web_sys::HtmlOptionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option)", childed); - component_impl!(Output, "output", web_sys::HtmlOutputElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output)", childed); - component_impl!(Paragraph, "p", web_sys::HtmlParagraphElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p)", childed); - component_impl!(Picture, "picture", web_sys::HtmlPictureElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)", childed); - component_impl!(Pre, "pre", web_sys::HtmlPreElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre)", childed); - component_impl!(Progress, "progress", web_sys::HtmlProgressElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress)", childed); - component_impl!(Quote, "q", web_sys::HtmlQuoteElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q)", childed); - component_impl!(Select, "select", web_sys::HtmlSelectElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)", childed); - component_impl!(Source, "source", web_sys::HtmlSourceElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source)", childless); - component_impl!(Span, "span", web_sys::HtmlSpanElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span)", childed); - component_impl!(Style, "style", web_sys::HtmlStyleElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)", childed); - component_impl!(Th, "th", web_sys::HtmlTableCellElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th)", childed); - component_impl!(Td, "td", web_sys::HtmlTableCellElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td)", childed); - component_impl!(Col, "col", web_sys::HtmlTableColElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col)", childed); - component_impl!(ColGroup, "colgroup", web_sys::HtmlTableColElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup)", childed); - component_impl!(Table, "table", web_sys::HtmlTableElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table)", childed); - component_impl!(Tr, "tr", web_sys::HtmlTableRowElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr)", childed); - component_impl!(THead, "thead", web_sys::HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead)", childed); - component_impl!(TFoot, "tfoot", web_sys::HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot)", childed); - component_impl!(TBody, "tbody", web_sys::HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody)", childed); - component_impl!(Template, "template", web_sys::HtmlTemplateElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)", childed); - component_impl!(TextArea, "textarea", web_sys::HtmlTextAreaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)", childed); - component_impl!(Time, "time", web_sys::HtmlTimeElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time)", childed); - component_impl!(Track, "track", web_sys::HtmlTrackElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)", childless); - component_impl!(Ul, "ul", web_sys::HtmlUListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul)", childed); - component_impl!(Video, "video", web_sys::HtmlVideoElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)", childed); + component_impl!(Anchor, "a", HtmlAnchorElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)", childed); + component_impl!(Area, "area", HtmlAreaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)", childless); + component_impl!(Audio, "audio", HtmlAudioElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio)", childed); + // component_impl!(Bold, "b", HtmlBElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b)", childed); + component_impl!(Br, "br", HtmlBrElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br)", childless); + component_impl!(Base, "base", HtmlBaseElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)", childless); + component_impl!(Button, "button", HtmlButtonElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)", childed); + component_impl!(Canvas, "canvas", HtmlCanvasElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)", childed); + component_impl!(Dl, "dl", HtmlDListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl)", childed); + component_impl!(Data, "data", HtmlDataElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data)", childed); + component_impl!(DataList, "datalist", HtmlDataListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)", childed); + component_impl!(Dialog, "dialog", HtmlDialogElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)", childed); + component_impl!(Div, "div", HtmlDivElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div)", childed); + component_impl!(Embed, "embed", HtmlEmbedElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed)", childless); + component_impl!(FieldSet, "fieldset", HtmlFieldSetElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)", childed); + component_impl!(Form, "form", HtmlFormElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)", childed); + component_impl!(FrameSet, "frameset", HtmlFrameSetElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frameset)", childed); + component_impl!(Hr, "hr", HtmlHrElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr)", childless); + component_impl!(H1, "h1", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)", childed); + component_impl!(H2, "h2", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2)", childed); + component_impl!(H3, "h3", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3)", childed); + component_impl!(H4, "h4", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4)", childed); + component_impl!(H5, "h5", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5)", childed); + component_impl!(H6, "h6", HtmlHeadingElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6)", childed); + component_impl!(Italic, "i", HtmlImageElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i)", childed); + component_impl!(IFrame, "iframe", HtmlIFrameElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)", childed); + component_impl!(Img, "img", HtmlImageElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img)", childed); + component_impl!(Input, "input", HtmlInputElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)", childless); + component_impl!(Li, "li", HtmlLiElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)", childed); + component_impl!(Label, "label", HtmlLabelElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)", childed); + component_impl!(Legend, "legend", HtmlLegendElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)", childed); + component_impl!(Link, "link", HtmlLinkElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)", childless); + component_impl!(Map, "map", HtmlMapElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map)", childed); + component_impl!(Meta, "meta", HtmlMetaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)", childless); + component_impl!(Meter, "meter", HtmlMeterElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter)", childed); + component_impl!(Ol, "ol", HtmlOListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol)", childed); + component_impl!(Object, "object", HtmlObjectElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)", childed); + component_impl!(OptGroup, "optgroup", HtmlOptGroupElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup)", childed); + component_impl!(Option, "option", HtmlOptionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option)", childed); + component_impl!(Output, "output", HtmlOutputElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output)", childed); + component_impl!(Paragraph, "p", HtmlParagraphElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p)", childed); + component_impl!(Picture, "picture", HtmlPictureElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)", childed); + component_impl!(Pre, "pre", HtmlPreElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre)", childed); + component_impl!(Progress, "progress", HtmlProgressElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress)", childed); + component_impl!(Quote, "q", HtmlQuoteElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q)", childed); + component_impl!(Select, "select", HtmlSelectElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)", childed); + component_impl!(Source, "source", HtmlSourceElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source)", childless); + component_impl!(Span, "span", HtmlSpanElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span)", childed); + component_impl!(Style, "style", HtmlStyleElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)", childed); + component_impl!(Th, "th", HtmlTableCellElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th)", childed); + component_impl!(Td, "td", HtmlTableCellElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td)", childed); + component_impl!(Col, "col", HtmlTableColElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col)", childed); + component_impl!(ColGroup, "colgroup", HtmlTableColElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup)", childed); + component_impl!(Table, "table", HtmlTableElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table)", childed); + component_impl!(Tr, "tr", HtmlTableRowElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr)", childed); + component_impl!(THead, "thead", HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead)", childed); + component_impl!(TFoot, "tfoot", HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot)", childed); + component_impl!(TBody, "tbody", HtmlTableSectionElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody)", childed); + component_impl!(Template, "template", HtmlTemplateElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)", childed); + component_impl!(TextArea, "textarea", HtmlTextAreaElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)", childed); + component_impl!(Time, "time", HtmlTimeElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time)", childed); + component_impl!(Track, "track", HtmlTrackElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)", childless); + component_impl!(Ul, "ul", HtmlUListElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul)", childed); + component_impl!(Video, "video", HtmlVideoElement, "[the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)", childed); } pub use impls::*; /// Make an HTML element with your own tag. pub struct CustomElement { - pub element: web_sys::HtmlElement, + pub element: dom::HtmlElement, } impl CustomElement { @@ -168,12 +174,19 @@ impl CustomElement { } pub fn render(&self, c: F) -> ContainerNodeFuture { - ContainerNodeFuture::new(c, AsRef::::as_ref(&self.element).clone()) + ContainerNodeFuture::new(c, AsRef::::as_ref(&self.element).clone()) } } +#[cfg(feature = "csr")] fn create_element(tag_name: &str) -> E { + use async_ui_web_core::window::DOCUMENT; DOCUMENT .with(|doc| doc.create_element(tag_name).unwrap_throw()) .unchecked_into() } + +#[cfg(any(feature = "ssr", not(feature = "csr")))] +fn create_element>(tag_name: &str) -> E { + E::try_from(dom::create_ssr_element(tag_name)).expect("TODO: HtmlElement: TryFrom error") +} diff --git a/async_ui_web_html/src/common_events.rs b/async_ui_web_html/src/common_events.rs index 67fc0ed..c7239fb 100644 --- a/async_ui_web_html/src/common_events.rs +++ b/async_ui_web_html/src/common_events.rs @@ -1,5 +1,6 @@ -use crate::events::{EmitEvent, EventFutureStream}; -use web_sys::{Element, HtmlElement}; +use crate::events::EventFutureStream; + +use async_ui_web_core::dom::{Element, HtmlElement}; macro_rules! make_event_impl { ($ev_name:literal, $func_name:ident, $ty:ty, $link:tt) => { @@ -11,7 +12,15 @@ macro_rules! make_event_impl { #[doc = $link] #[doc = "."] fn $func_name(&self) -> EventFutureStream<$ty> { - self.as_ref().until_event($ev_name.into()) + #[cfg(feature = "csr")] + { + use crate::events::EmitEvent; + return self.as_ref().until_event($ev_name.into()) + } + #[cfg(not(feature = "csr"))] + { + EventFutureStream::new_dummy($ev_name.into()) + } } }; } diff --git a/async_ui_web_html/src/event_handling.rs b/async_ui_web_html/src/event_handling.rs index 28c1275..4634e11 100644 --- a/async_ui_web_html/src/event_handling.rs +++ b/async_ui_web_html/src/event_handling.rs @@ -7,9 +7,11 @@ use std::{ }; use async_ui_internal_utils::dummy_waker::dummy_waker; +use async_ui_web_core::dom::EventTarget; use futures_core::Stream; -use wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}; -use web_sys::{AddEventListenerOptions, EventTarget}; +use wasm_bindgen::JsCast; +#[cfg(feature = "csr")] +use wasm_bindgen::{closure::Closure, UnwrapThrowExt}; /// A struct implementing both [Future] and [Stream]. /// Yields [Event][web_sys::Event] objects. @@ -25,14 +27,40 @@ use web_sys::{AddEventListenerOptions, EventTarget}; /// fail to poll the Stream upon `wake`, you might miss some /// in-between events. pub struct EventFutureStream { + #[allow(dead_code)] target: EventTarget, + + #[cfg(feature = "csr")] closure: Option>, + #[cfg(not(feature = "csr"))] + closure: Option<()>, + shared: Rc, Waker)>>, - options: Option, + + #[cfg(feature = "csr")] + options: Option, + + #[allow(dead_code)] event_name: Cow<'static, str>, } impl EventFutureStream { + pub fn new_dummy(event_name: Cow<'static, str>) -> Self { + use async_ui_web_core::dom; + + Self { + #[cfg(feature = "csr")] + target: web_sys::EventTarget::new().unwrap(), + #[cfg(not(feature = "csr"))] + target: dom::SsrEventTarget {}, + closure: None, + shared: Rc::new(RefCell::new((None, dummy_waker()))), + #[cfg(feature = "csr")] + options: None, + event_name, + } + } + /// Prefer to use [until_event][crate::events::EmitEvent::until_event] or other until_* /// methods instead of this. pub fn new(target: EventTarget, event_name: Cow<'static, str>) -> Self { @@ -40,6 +68,7 @@ impl EventFutureStream { target, closure: None, shared: Rc::new(RefCell::new((None, dummy_waker()))), + #[cfg(feature = "csr")] options: None, event_name, } @@ -53,9 +82,13 @@ impl EventFutureStream { /// /// This needs to be set *before* you first poll the stream. pub fn set_capture(&mut self, capture: bool) { - self.options - .get_or_insert_with(AddEventListenerOptions::new) - .capture(capture); + #[cfg(feature = "csr")] + { + self.options + .get_or_insert_with(web_sys::AddEventListenerOptions::new) + .capture(capture); + } + let _ = capture; } /// The `passive` option indicates that the function specified by listener /// will never call `preventDefault()`. @@ -68,9 +101,13 @@ impl EventFutureStream { /// /// This needs to be set *before* you first poll the stream. pub fn set_passive(&mut self, passive: bool) { - self.options - .get_or_insert_with(AddEventListenerOptions::new) - .passive(passive); + #[cfg(feature = "csr")] + { + self.options + .get_or_insert_with(web_sys::AddEventListenerOptions::new) + .passive(passive); + } + let _ = passive; } } @@ -102,30 +139,39 @@ impl Stream for EventFutureStream { } if this.closure.is_none() { - let shared_weak = Rc::downgrade(&this.shared); - let closure = Closure::new(move |ev: web_sys::Event| { - if let Some(strong) = shared_weak.upgrade() { - let inner = &mut *strong.borrow_mut(); - inner.0 = Some(ev.unchecked_into()); - inner.1.wake_by_ref(); + #[cfg(feature = "csr")] + { + let shared_weak = Rc::downgrade(&this.shared); + let closure = Closure::new(move |ev: web_sys::Event| { + if let Some(strong) = shared_weak.upgrade() { + let inner = &mut *strong.borrow_mut(); + inner.0 = Some(ev.unchecked_into()); + inner.1.wake_by_ref(); + } + async_ui_web_core::executor::run_now(); + }); + let listener = closure.as_ref().unchecked_ref(); + if let Some(options) = &this.options { + this.target + .add_event_listener_with_callback_and_add_event_listener_options( + &this.event_name, + listener, + options, + ) + .unwrap_throw(); + } else { + this.target + .add_event_listener_with_callback(&this.event_name, listener) + .unwrap_throw(); } - async_ui_web_core::executor::run_now(); - }); - let listener = closure.as_ref().unchecked_ref(); - if let Some(options) = &this.options { - this.target - .add_event_listener_with_callback_and_add_event_listener_options( - &this.event_name, - listener, - options, - ) - .unwrap_throw(); - } else { - this.target - .add_event_listener_with_callback(&this.event_name, listener) - .unwrap_throw(); + this.closure = Some(closure); + } + + #[cfg(feature = "ssr")] + { + this.target.event_subscribed(&this.event_name); + this.closure = Some(()); } - this.closure = Some(closure); Poll::Pending } else if let Some(ev) = this.shared.borrow_mut().0.take() { Poll::Ready(Some(ev)) @@ -158,13 +204,22 @@ impl EmitEvent for EventTarget { impl Drop for EventFutureStream { fn drop(&mut self) { + #[allow(unused_variables)] if let Some(callback) = self.closure.take() { - self.target - .remove_event_listener_with_callback( - &self.event_name, - callback.as_ref().unchecked_ref(), - ) - .unwrap_throw(); + #[cfg(feature = "csr")] + { + self.target + .remove_event_listener_with_callback( + &self.event_name, + callback.as_ref().unchecked_ref(), + ) + .unwrap_throw(); + } + #[cfg(feature = "ssr")] + { + self.target.event_unsubscribed(&self.event_name); + let _: () = callback; + } } } } diff --git a/async_ui_web_html/src/text_node.rs b/async_ui_web_html/src/text_node.rs index b8dff0c..25bd8fd 100644 --- a/async_ui_web_html/src/text_node.rs +++ b/async_ui_web_html/src/text_node.rs @@ -3,17 +3,20 @@ use std::{ ops::Deref, }; -use async_ui_web_core::{window::DOCUMENT, ContainerNodeFuture}; +use async_ui_web_core::{dom, ContainerNodeFuture}; /// An HTML text node. pub struct Text { - pub node: web_sys::Text, + pub node: dom::Text, } impl Text { pub fn new() -> Self { Self { - node: DOCUMENT.with(|doc| doc.create_text_node("")), + #[cfg(feature = "csr")] + node: async_ui_web_core::window::DOCUMENT.with(|doc| doc.create_text_node("")), + #[cfg(not(feature = "csr"))] + node: dom::create_ssr_text("") } } pub fn render(&self) -> ContainerNodeFuture> { @@ -28,7 +31,7 @@ impl Default for Text { } impl Deref for Text { - type Target = web_sys::Text; + type Target = dom::Text; fn deref(&self) -> &Self::Target { &self.node diff --git a/async_ui_web_macros/src/lib.rs b/async_ui_web_macros/src/lib.rs index d555a20..374a050 100644 --- a/async_ui_web_macros/src/lib.rs +++ b/async_ui_web_macros/src/lib.rs @@ -4,6 +4,8 @@ mod select; use select::select_macro; /// Register CSS to be bundled and generate postfixed classnames. +// TODO: Incompatible with SSR, should there be some `let _style: StyleGuard = use_style(css_mod::STYLE)`, where `StyleGuard` maintains refcnt +// of used styles, and inserts used styles to used css style list context? #[proc_macro] pub fn css(input: proc_macro::TokenStream) -> proc_macro::TokenStream { css_macro(input) diff --git a/examples/login-flow/Cargo.toml b/examples/login-flow/Cargo.toml index c0ac916..d6592f5 100644 --- a/examples/login-flow/Cargo.toml +++ b/examples/login-flow/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -crate-type = ["cdylib"] +crate-type = ["rlib", "cdylib"] [dependencies] async_ui_web = { path = "../../async_ui_web/" } @@ -15,3 +15,6 @@ futures-lite = "1.13.0" console_error_panic_hook = "0.1.6" gloo-timers = { version = "0.2", features = ["futures"] } +[features] +csr = ["async_ui_web/csr"] +ssr = ["async_ui_web/ssr"] diff --git a/examples/login-flow/src/lib.rs b/examples/login-flow/src/lib.rs index 4925368..c29fd3d 100644 --- a/examples/login-flow/src/lib.rs +++ b/examples/login-flow/src/lib.rs @@ -1,15 +1,18 @@ use async_ui_web::{ event_traits::EmitElementEvent, html::{Button, Input}, - join, mount, race, + join, race, shortcut_traits::{ShortcutRenderStr, UiFutureExt}, }; #[wasm_bindgen::prelude::wasm_bindgen(start)] +#[cfg(feature = "csr")] pub fn run() { + use async_ui_web::mount; mount(app()); } +#[allow(dead_code)] async fn app() { match connector().await { Ok(data) => format!("the connector returned: {data}").render().await, diff --git a/examples/web-todomvc/Cargo.toml b/examples/web-todomvc/Cargo.toml index b6afb2e..ddd7c9a 100644 --- a/examples/web-todomvc/Cargo.toml +++ b/examples/web-todomvc/Cargo.toml @@ -6,9 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "ssr" [dependencies] async_ui_web = { path = "../../async_ui_web/" } wasm-bindgen = "0.2.82" futures-lite = "1.13.0" + +[features] +csr = ["async_ui_web/csr"] +ssr = ["async_ui_web/ssr"] diff --git a/examples/web-todomvc/index.html b/examples/web-todomvc/index.html index c1e1749..535295c 100644 --- a/examples/web-todomvc/index.html +++ b/examples/web-todomvc/index.html @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/examples/web-todomvc/index_hydrate.html b/examples/web-todomvc/index_hydrate.html new file mode 100644 index 0000000..0f467e3 --- /dev/null +++ b/examples/web-todomvc/index_hydrate.html @@ -0,0 +1,20 @@ + + + + + + + + + + + +

todos

Double-click to edit a todo

Written with Async UI.

+ + + diff --git a/examples/web-todomvc/src/bin/ssr.rs b/examples/web-todomvc/src/bin/ssr.rs new file mode 100644 index 0000000..156850a --- /dev/null +++ b/examples/web-todomvc/src/bin/ssr.rs @@ -0,0 +1,15 @@ + +#[cfg(feature = "ssr")] +fn main() { + use async_ui_web::render_to_string; + use web_todomvc::app::app; + let v = render_to_string(app()); + + let v = futures_lite::future::block_on(v); + println!("{v}"); +} + +#[cfg(not(feature = "ssr"))] +fn main() { + panic!("ssr requires ssr feature") +} diff --git a/examples/web-todomvc/src/lib.rs b/examples/web-todomvc/src/lib.rs index a85bcd1..132b447 100644 --- a/examples/web-todomvc/src/lib.rs +++ b/examples/web-todomvc/src/lib.rs @@ -1,11 +1,10 @@ -use async_ui_web::mount; -use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; +pub mod app; -mod app; -use app::app; +#[cfg(feature = "csr")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn run() -> Result<(), wasm_bindgen::JsValue> { + use async_ui_web::mount; -#[wasm_bindgen(start)] -pub fn run() -> Result<(), JsValue> { - mount(app()); + mount(app::app()); Ok(()) } diff --git a/examples/x-bow-playground/Cargo.toml b/examples/x-bow-playground/Cargo.toml index be73bc8..5214797 100644 --- a/examples/x-bow-playground/Cargo.toml +++ b/examples/x-bow-playground/Cargo.toml @@ -20,4 +20,9 @@ futures-lite = "1.13.0" version = "0.3.56" features = [ 'console' -] \ No newline at end of file +] + +[features] +default = ["csr"] +csr = ["async_ui_web/csr"] +ssr = [] diff --git a/examples/x-bow-playground/src/lib.rs b/examples/x-bow-playground/src/lib.rs index 7e6235a..9963d02 100644 --- a/examples/x-bow-playground/src/lib.rs +++ b/examples/x-bow-playground/src/lib.rs @@ -3,7 +3,7 @@ use std::{fmt::Debug, future::pending, pin::Pin}; use async_ui_web::{ event_traits::EmitElementEvent, html::{Anchor, Button, Div, Span, H3}, - join, mount, + join, shortcut_traits::{ShortcutClassList, ShortcutClassListBuilder, ShortcutRenderStr}, ReactiveCell, }; @@ -128,7 +128,10 @@ impl CanDisplay for Leaf { } #[wasm_bindgen(start)] +#[cfg(feature = "csr")] pub fn run() { + use async_ui_web::mount; + console_error_panic_hook::set_once(); mount(app()); } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..64264da --- /dev/null +++ b/flake.lock @@ -0,0 +1,98 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1736566337, + "narHash": "sha256-SC0eDcZPqISVt6R0UfGPyQLrI0+BppjjtQ3wcSlk0oI=", + "owner": "ipetkov", + "repo": "crane", + "rev": "9172acc1ee6c7e1cbafc3044ff850c568c75a5a3", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1736722550, + "narHash": "sha256-rpncbQb8WwnR7xmWe+leR8NFBJ5cfPbklfArPjITd1s=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fc6b5c7cd8158d21e0dca0e4c6701ce021712eef", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736700680, + "narHash": "sha256-9gmWIb8xsycWHEYpd2SiVIAZnUULX6Y+IMMZBcDUCQU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "5d1865c0da63b4c949f383d982b6b43519946e8f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..04130ae --- /dev/null +++ b/flake.nix @@ -0,0 +1,64 @@ +{ + description = "Auth service"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/master"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + crane = { + url = "github:ipetkov/crane"; + }; + }; + outputs = { + nixpkgs, + flake-utils, + rust-overlay, + crane, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + overlays = [rust-overlay.overlays.default]; + config.allowUnfree = true; + }; + rust = + (pkgs.rustChannelOf { + date = "2024-11-06"; + channel = "nightly"; + }) + .default + .override { + extensions = ["rust-analyzer" "rust-src" "rustc-codegen-cranelift-preview"]; + targets = ["wasm32-unknown-unknown"]; + }; + craneLib = ((crane.mkLib pkgs).overrideToolchain rust).overrideScope (_final: _prev: { + inherit (pkgs) wasm-bindgen-cli; + }); + in rec { + serverDeps = with pkgs; [ + rustPlatform.bindgenHook + + wasm-bindgen-cli + ]; + devShell = pkgs.mkShell { + nativeBuildInputs = with pkgs; + [ + alejandra + rust + cargo-expand + cargo-watch + cargo-edit + gdb + wasm-pack + miniserve + ] + ++ serverDeps; + RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; + }; + } + ); +}