From 28ac37a8b23874c77011a46a11e6b9cbdf79ecdd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 21 Jan 2021 02:25:44 -0500 Subject: [PATCH] Feat: update fc_macro --- .vscode/spellright.dict | 1 + CHANGELOG.md | 3 +- Cargo.toml | 1 + README.md | 16 +- examples/Cargo.toml | 5 + examples/fc_macro.rs | 19 ++ packages/core-macro/Cargo.toml | 2 +- packages/core-macro/src/lib.rs | 402 +++++++++++++++++++-------------- packages/dioxus/Cargo.toml | 1 + packages/dioxus/src/lib.rs | 50 +--- packages/ssr/Cargo.toml | 5 +- packages/ssr/src/lib.rs | 78 ++++++- 12 files changed, 353 insertions(+), 230 deletions(-) create mode 100644 examples/fc_macro.rs diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 0e62c3486..c157c79ac 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -3,3 +3,4 @@ Clippy tide_ssr Liveview Dioxus +VDoms diff --git a/CHANGELOG.md b/CHANGELOG.md index e70f3a0d8..b39b0acdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,10 @@ - [ ] (SSR) Implement stateful 3rd party string renderer - [ ] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" in place of {format!("Text is {}",blah)} -# Project: Initial Release (TBD) +# Project: Initial VDOM support (TBD) > Get the initial VDom + Event System + Patching + Diffing + Component framework up and running - [x] (Core) Migrate virtual node into new VNode type +- [ ] (Macro) Allow components to specify their props as function args - [ ] (Core) Arena allocate VNodes - [ ] (Core) Allow VNodes to borrow arena contents - [ ] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures) diff --git a/Cargo.toml b/Cargo.toml index 7aa77ea52..9b31c251f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "packages/redux", "packages/core-macro", "packages/router", + "packages/ssr", # "packages/macro", # TODO @Jon, share the validation code # "packages/web", diff --git a/README.md b/README.md index 6e5b34b1c..3d279fa42 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ # About -Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented as a tree of Virtual Nodes not bound to any specific renderer. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers used to crafting react-based experiences should feel comfortable with Dioxus. +Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented as a tree of Virtual Nodes not bound to any specific renderer. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers experienced with building react-based experiences should feel comfortable with Dioxus. Dioxus is unique in the space of UI for Rust. Dioxus supports a renderer approach called "broadcasting" where two VDoms with separate renderers can sync their UI states remotely. Our goal as a framework is to work towards "Dioxus Liveview" where a server and client work in tandem, eliminating the need for frontend-specific APIs altogether. ## Features -Dioxus' goal is to be the most advanced UI system for Rust, targeting isomorphism and hybrid approaches. Our goal is to never be burdened with context switching between various programming languages, build tools, and environment idiosyncrasies. +Dioxus' goal is to be the most advanced UI system for Rust, targeting isomorphism and hybrid approaches. Our goal is to eliminate context-switching for cross-platform development - both in UI patterns and programming language. Hooks and components should work *everywhere* without compromise. Dioxus Core supports: - [ ] Hooks @@ -21,7 +21,7 @@ Dioxus Core supports: - [ ] State management integrations On top of these, we have several projects you can find in the `packages` folder. -- [x] `dioxuscli`: Testing, development, and packaging tools for Dioxus apps +- [x] `dioxus-cli`: Testing, development, and packaging tools for Dioxus apps - [ ] `dioxus-router`: A hook-based router implementation for Dioxus web apps - [ ] `Dioxus-vscode`: Syntax highlighting, code formatting, and hints for Dioxus html! blocks - [ ] `Redux-rs`: Redux-style global state management @@ -47,19 +47,19 @@ fn Example(ctx: &Context) -> VNode { } ``` -Here, the `Context` object is used to access hook state, create subscriptions, and interact with the built-in context API. Props, children, and component APIs are accessible via the `Context` object. If using the functional component macro, it's possible to inline props into the function definition itself. +Here, the `Context` object is used to access hook state, create subscriptions, and interact with the built-in context API. Props, children, and component APIs are accessible via the `Context` object. The functional component macro makes life more productive by inlining props directly as function arguments, similar to how Rocket parses URIs. ```rust // A very terse component! -#[functional_component] -fn Example(ctx: &Context<{ name: String }>) -> VNode { +#[fc] +fn Example(ctx: &Context, name: String) -> VNode { html! {
"Hello {name}!"
} } // or #[functional_component] -static Example: FC<{ name: String }> = |ctx| html! {
"Hello {:?name}!"
}; +static Example: FC = |ctx, name: String| html! {
"Hello {:?name}!"
}; ``` The final output of components must be a tree of VNodes. We provide an html macro for using JSX-style syntax to write these, though, you could use any macro, DSL, or templating engine. Work is being done on a terra template processor for existing templates. @@ -83,7 +83,7 @@ async fn user_data(ctx: &Context<()>) -> VNode { Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components. ## Examples -We use `diopack` to build and test webapps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/diopack` will get it installed. +We use the dedicated `dioxus-cli` to build and test dioxus web-apps. This can run examples, tests, build web workers, launch development servers, bundle, and more. It's general purpose, but currently very tailored to Dioxus for liveview and bundling. If you've not used it before, `cargo install --path pacakages/dioxus-cli` will get it installed. This CLI tool should feel like using `cargo` but with 1st party support for assets, bundling, and other important dioxus-specific features. Alternatively, `trunk` works but can't run examples. diff --git a/examples/Cargo.toml b/examples/Cargo.toml index cd3bf37af..e97502bfc 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" fern = { version = "0.6.0", features = ["colored"] } log = "0.4.1" dioxus = { path = "../packages/dioxus" } +dioxus-ssr = { path = "../packages/ssr" } rand = "0.8.2" @@ -43,3 +44,7 @@ name = "doc_generator" [[example]] path = "router.rs" name = "router" + +[[example]] +path = "fc_macro.rs" +name = "fc_macro" diff --git a/examples/fc_macro.rs b/examples/fc_macro.rs new file mode 100644 index 000000000..9e6a6990a --- /dev/null +++ b/examples/fc_macro.rs @@ -0,0 +1,19 @@ +use dioxus::prelude::*; +use dioxus_ssr::TextRenderer; + +// todo @Jon, support components in the html! macro +// let renderer = TextRenderer::new(|_| html! {}); +fn main() { + let renderer = TextRenderer::<()>::new(|_| html! {
"Hello world"
}); + let output = renderer.render(); +} + +/// An example component that demonstrates how to use the functional_component macro +/// This macro makes writing functional components elegant, similar to how Rocket parses URIs. +/// +/// You don't actually *need* this macro to be productive, but it makes life easier, and components cleaner. +/// This approach also integrates well with tools like Rust-Analyzer +#[fc] +fn example(ctx: &Context, name: String) -> VNode { + html! {
"Hello, {name}!"
} +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index c67da6442..77a4dd333 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core-macro" -version = "0.0.0" +version = "0.1.0" authors = ["Jonathan Kelley "] edition = "2018" diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index eeb335574..2b268c477 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,14 +1,118 @@ use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; -use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; +use syn::{ + parse::{Parse, ParseStream}, + Signature, +}; use syn::{ parse_macro_input, Attribute, Block, FnArg, Ident, Item, ItemFn, ReturnType, Type, Visibility, }; +/// Label a function or static closure as a functional component. +/// This macro reduces the need to create a separate properties struct. +#[proc_macro_attribute] +pub fn fc(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let item = parse_macro_input!(item as FunctionComponent); + // let attr = parse_macro_input!(attr as FunctionComponentName); + + function_component_impl(item) + // function_component_impl(attr, item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +fn function_component_impl( + // name: FunctionComponentName, + component: FunctionComponent, +) -> syn::Result { + // let FunctionComponentName { component_name } = name; + + let FunctionComponent { + block, + props_type, + arg, + vis, + attrs, + name: function_name, + return_type, + } = component; + + // if function_name == component_name { + // return Err(syn::Error::new_spanned( + // component_name, + // "the component must not have the same name as the function", + // )); + // } + + let ret_type = quote_spanned!(return_type.span()=> VNode); + // let ret_type = quote_spanned!(return_type.span()=> ::VNode); + // let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html); + + let quoted = quote! { + #[doc(hidden)] + #[allow(non_camel_case_types)] + mod __component_blah { + use super::*; + + #[derive(PartialEq)] + pub struct Props { + name: String + } + + pub fn component(ctx: &mut Context) -> #ret_type { + let Props { + name + } = ctx.props; + #block + } + } + #[allow(non_snake_case)] + pub use __component_blah::component as #function_name; + + + + + // #vis struct #function_name; + + // impl ::yew_functional::FunctionProvider for #function_name { + // type TProps = #props_type; + + // fn run(#arg) -> #ret_type { + // #block + // } + // } + + // #(#attrs)* + // #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>; + }; + // let quoted = quote! { + // #[doc(hidden)] + // #[allow(non_camel_case_types)] + // #vis struct #function_name; + + // impl ::yew_functional::FunctionProvider for #function_name { + // type TProps = #props_type; + + // fn run(#arg) -> #ret_type { + // #block + // } + // } + + // #(#attrs)* + // #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>; + // }; + Ok(quoted) +} + +/// A parsed version of the user's input struct FunctionComponent { + // The actual contents of the function block: Box, + + // The user's props type props_type: Box, + arg: FnArg, vis: Visibility, attrs: Vec, @@ -20,123 +124,150 @@ impl Parse for FunctionComponent { fn parse(input: ParseStream) -> syn::Result { let parsed: Item = input.parse()?; - match parsed { - Item::Fn(func) => { - let ItemFn { - attrs, - vis, - sig, - block, - } = func; + // Convert the parsed input into the Function block + let ItemFn { + attrs, + vis, + sig, + block, + } = ensure_fn_block(parsed)?; - if !sig.generics.params.is_empty() { - return Err(syn::Error::new_spanned( - sig.generics, - "function components can't contain generics", - )); - } + // Validate the user's signature + let sig = validate_signature(sig)?; - if sig.asyncness.is_some() { - return Err(syn::Error::new_spanned( - sig.asyncness, - "function components can't be async", - )); - } + // Validate the return type is actually something + let return_type = ensure_return_type(sig.output)?; - if sig.constness.is_some() { - return Err(syn::Error::new_spanned( - sig.constness, - "const functions can't be function components", - )); - } + // Get all the function args + let mut inputs = sig.inputs.into_iter(); - if sig.abi.is_some() { - return Err(syn::Error::new_spanned( - sig.abi, - "extern functions can't be function components", - )); - } + // Collect the first arg + let first_arg: FnArg = inputs + .next() + .unwrap_or_else(|| syn::parse_quote! { _: &() }); - let return_type = match sig.output { - ReturnType::Default => { - return Err(syn::Error::new_spanned( - sig, - "function components must return `yew::Html`", - )) - } - ReturnType::Type(_, ty) => ty, - }; + // Extract the "context" object + let props_type = validate_context_arg(&first_arg)?; - let mut inputs = sig.inputs.into_iter(); - let arg: FnArg = inputs - .next() - .unwrap_or_else(|| syn::parse_quote! { _: &() }); + // Collect the rest of the args into a list of definitions to be used by the inline struct - let ty = match &arg { - FnArg::Typed(arg) => match &*arg.ty { - Type::Reference(ty) => { - if ty.lifetime.is_some() { - return Err(syn::Error::new_spanned( - &ty.lifetime, - "reference must not have a lifetime", - )); - } + // Checking after param parsing may make it a little inefficient + // but that's a requirement for better error messages in case of receivers + // `>0` because first one is already consumed. + // if inputs.len() > 0 { + // let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); + // return Err(syn::Error::new_spanned( + // params, + // "function components can accept at most one parameter for the props", + // )); + // } + let name = sig.ident; - if ty.mutability.is_some() { - return Err(syn::Error::new_spanned( - &ty.mutability, - "reference must not be mutable", - )); - } - - ty.elem.clone() - } - ty => { - let msg = format!( - "expected a reference to a `Properties` type (try: `&{}`)", - ty.to_token_stream() - ); - return Err(syn::Error::new_spanned(ty, msg)); - } - }, - - FnArg::Receiver(_) => { - return Err(syn::Error::new_spanned( - arg, - "function components can't accept a receiver", - )); - } - }; - - // Checking after param parsing may make it a little inefficient - // but that's a requirement for better error messages in case of receivers - // `>0` because first one is already consumed. - if inputs.len() > 0 { - let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); - return Err(syn::Error::new_spanned( - params, - "function components can accept at most one parameter for the props", - )); - } - - Ok(Self { - props_type: ty, - block, - arg, - vis, - attrs, - name: sig.ident, - return_type, - }) - } - item => Err(syn::Error::new_spanned( - item, - "`function_component` attribute can only be applied to functions", - )), - } + Ok(Self { + props_type, + block, + arg: first_arg, + vis, + attrs, + name, + return_type, + }) } } +/// Ensure the user's input is actually a functional component +fn ensure_fn_block(item: Item) -> syn::Result { + match item { + Item::Fn(it) => Ok(it), + other => Err(syn::Error::new_spanned( + other, + "`function_component` attribute can only be applied to functions", + )), + } +} + +/// Ensure the user's function actually returns a VNode +fn ensure_return_type(output: ReturnType) -> syn::Result> { + match output { + ReturnType::Default => Err(syn::Error::new_spanned( + output, + "function components must return `dioxus::VNode`", + )), + ReturnType::Type(_, ty) => Ok(ty), + } +} + +/// Validate the users's input signature for the function component. +/// Returns an error if any of the conditions prove to be wrong; +fn validate_signature(sig: Signature) -> syn::Result { + if !sig.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + sig.generics, + "function components can't contain generics", + )); + } + + if sig.asyncness.is_some() { + return Err(syn::Error::new_spanned( + sig.asyncness, + "function components can't be async", + )); + } + + if sig.constness.is_some() { + return Err(syn::Error::new_spanned( + sig.constness, + "const functions can't be function components", + )); + } + + if sig.abi.is_some() { + return Err(syn::Error::new_spanned( + sig.abi, + "extern functions can't be function components", + )); + } + + Ok(sig) +} + +fn validate_context_arg(first_arg: &FnArg) -> syn::Result> { + if let FnArg::Typed(arg) = first_arg { + // Input arg is a reference to an &mut Context + if let Type::Reference(ty) = &*arg.ty { + if ty.lifetime.is_some() { + return Err(syn::Error::new_spanned( + &ty.lifetime, + "reference must not have a lifetime", + )); + } + + if ty.mutability.is_some() { + return Err(syn::Error::new_spanned( + &ty.mutability, + "reference must not be mutable", + )); + } + + Ok(ty.elem.clone()) + } else { + let msg = format!( + "expected a reference to a `Context` object (try: `&mut {}`)", + arg.ty.to_token_stream() + ); + return Err(syn::Error::new_spanned(arg.ty.clone(), msg)); + } + } else { + return Err(syn::Error::new_spanned( + first_arg, + "function components can't accept a receiver", + )); + } +} + +fn collect_inline_args() {} + +/// The named specified in the macro usage. struct FunctionComponentName { component_name: Ident, } @@ -152,60 +283,3 @@ impl Parse for FunctionComponentName { Ok(Self { component_name }) } } - -#[proc_macro_attribute] -pub fn function_component( - attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - let item = parse_macro_input!(item as FunctionComponent); - let attr = parse_macro_input!(attr as FunctionComponentName); - - function_component_impl(attr, item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - -fn function_component_impl( - name: FunctionComponentName, - component: FunctionComponent, -) -> syn::Result { - let FunctionComponentName { component_name } = name; - - let FunctionComponent { - block, - props_type, - arg, - vis, - attrs, - name: function_name, - return_type, - } = component; - - if function_name == component_name { - return Err(syn::Error::new_spanned( - component_name, - "the component must not have the same name as the function", - )); - } - - let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html); - - let quoted = quote! { - #[doc(hidden)] - #[allow(non_camel_case_types)] - #vis struct #function_name; - - impl ::yew_functional::FunctionProvider for #function_name { - type TProps = #props_type; - - fn run(#arg) -> #ret_type { - #block - } - } - - #(#attrs)* - #vis type #component_name = ::yew_functional::FunctionComponent<#function_name>; - }; - Ok(quoted) -} diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index b50cef952..4d412ae0c 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -9,3 +9,4 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi [dependencies] dioxus-core = { path = "../core", version = "0.1.0" } +dioxus-core-macro = { path = "../core-macro", version = "0.1.0" } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index bf350b5ce..cb1b61434 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -1,57 +1,9 @@ pub mod prelude { pub use dioxus_core::prelude::*; + pub use dioxus_core_macro::fc; } use dioxus_core::prelude::FC; // Re-export core completely pub use dioxus_core as core; - -struct App {} -impl App { - fn at(&mut self, pat: &str) -> Route { - todo!() - } - - // start the app in a new thread - // pass updates to it via state manager? - fn start(self) {} -} - -fn new() -> App { - todo!() -} - -struct Router {} - -struct Route {} - -struct RouteInfo {} - -impl RouteInfo { - fn query(&self) -> T { - todo!() - } -} - -impl Route { - fn serve(&self, app: FC) {} -} - -#[cfg(test)] -mod test { - use super::*; - use crate::prelude::*; - - #[test] - fn app_example() { - let mut app = crate::new(); - - app.at("/").serve(|ctx| { - let parsed: u32 = ctx.props.query(); - html! {
"hello "
} - }); - - app.start(); - } -} diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 1d0c41420..3b1ae961f 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "dioxus-ssr" -version = "0.0.0" +version = "0.1.0" authors = ["Jonathan Kelley "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] \ No newline at end of file +[dependencies] +dioxus-core = { path = "../core", version = "0.1.0" } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 31e1bb209..b6106ac6c 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -1,7 +1,75 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +//! Dioxus Server-Side-Rendering +//! +//! This crate demonstrates how to implement a custom renderer for Dioxus VNodes via the `TextRenderer` renderer. +//! The `TextRenderer` consumes a Dioxus Virtual DOM, progresses its event queue, and renders the VNodes to a String. +//! +//! While `VNode` supports "to_string" directly, it renders child components as the RSX! macro tokens. For custom components, +//! an external renderer is needed to progress the component lifecycles. The `TextRenderer` shows how to use the Virtual DOM +//! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the +//! `render` method. +//! +//! ```ignore +//! fn main() { +//! let renderer = TextRenderer::<()>::new(|_| html! {
"Hello world"
}); +//! let output = renderer.render(); +//! assert_eq!(output, "
Hello World
"); +//! } +//! ``` +//! +//! The `TextRenderer` is particularly useful when needing to cache a Virtual DOM in between requests +//! + +use dioxus_core::prelude::FC; + +/// The `TextRenderer` provides a way of rendering a Dioxus Virtual DOM to a String. +/// +/// +/// +pub struct TextRenderer { + _root_type: std::marker::PhantomData, +} + +impl TextRenderer { + /// Create a new text-renderer instance from a functional component root. + /// Automatically progresses the creation of the VNode tree to completion. + /// + /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` + pub fn new(root: FC) -> Self { + Self { + _root_type: std::marker::PhantomData {}, + } + } + + /// Create a new text renderer from an existing Virtual DOM. + /// This will progress the existing VDom's events to completion. + pub fn from_vdom() -> Self { + todo!() + } + + /// Pass new args to the root function + pub fn update(&mut self, new_val: T) { + todo!() + } + + /// Modify the root function in place, forcing a re-render regardless if the props changed + pub fn update_mut(&mut self, modifier: impl Fn(&mut T)) { + todo!() + } + + /// Render the virtual DOM to a string + pub fn render(&self) -> String { + let mut buffer = String::new(); + + // iterate through the internal patch queue of virtual dom, and apply them to the buffer + /* + */ + todo!() + } + + /// Render VDom to an existing buffer + /// TODO @Jon, support non-string buffers to actually make this useful + /// Currently, this only supports overwriting an existing buffer, instead of just + pub fn render_mut(&self, buf: &mut String) { + todo!() } }