Merge branch 'master' into desktop-serve-cli

This commit is contained in:
Jonathan Kelley committed 2023-07-19 13:33:02 -07:00
commit 9185afdba8
236 files changed
+11396 -4784

No files matched your search

-1
View File
@@ -48,7 +48,6 @@ impl Writer<'_> {
key,
attributes,
children,
_is_static,
brace,
} = el;
-7
View File
@@ -88,13 +88,6 @@ interprocess-docfix = { version = "1.2.2" }
default = []
plugin = ["mlua"]
# install path dx and dioxus as the same command
# so, they're not really aliases
# eventually dx will defer to the right version of dioxus
[[bin]]
path = "src/main.rs"
name = "dioxus"
[[bin]]
path = "src/main.rs"
name = "dx"
+2 -2
View File
@@ -4,7 +4,7 @@
name = "dioxus-cli"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# you can also use `dx serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "desktop"
@@ -42,4 +42,4 @@ script = []
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = true }
binaryen = { wasm_opt = true }
+2 -2
View File
@@ -24,7 +24,7 @@ cargo install --path . --debug
## Get Started
Use `dioxus create project-name` to initialize a new Dioxus project. <br>
Use `dx create project-name` to initialize a new Dioxus project. <br>
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository.
@@ -33,7 +33,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path:
```
dioxus create hello --template gh:dioxuslabs/dioxus-template
dx create hello --template gh:dioxuslabs/dioxus-template
```
## Dioxus Config File
+4 -4
View File
@@ -2,14 +2,14 @@
In this chapter we will introduce all `dioxus-cli` commands.
> You can also use `dioxus --help` to get cli help info.
> You can also use `dx --help` to get cli help info.
```
dioxus
dx
Build, bundle, & ship your Dioxus app
USAGE:
dioxus [OPTIONS] <SUBCOMMAND>
dx [OPTIONS] <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
@@ -23,4 +23,4 @@ SUBCOMMANDS:
help Print this message or the help of the given subcommand(s)
serve Build, watch & serve the Rust WASM app and all of its assets
translate Translate some html file into a Dioxus component
```
```
+9 -9
View File
@@ -1,13 +1,13 @@
# Build
The `dioxus build` command can help you `pack & build` a dioxus project.
The `dx build` command can help you `pack & build` a dioxus project.
```
dioxus-build
dioxus-build
Build the Rust WASM app and all of its assets
USAGE:
dioxus build [OPTIONS]
dx build [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
@@ -19,7 +19,7 @@ OPTIONS:
You can use this command to build a project:
```
dioxus build --release
dx build --release
```
## Target platform
@@ -28,14 +28,14 @@ Use the `platform` option to choose your target platform:
```
# for desktop project
dioxus build --platform desktop
dx build --platform desktop
```
`platform` currently only supports `desktop` & `web`.
```
# for web project
dioxus build --platform web
dx build --platform web
```
## Specify workspace bin
@@ -43,7 +43,7 @@ dioxus build --platform web
You can add the `--bin` option to select which crate you want Dioxus to build:
```
dioxus build --bin app
dx build --bin app
```
## Build Example
@@ -52,5 +52,5 @@ You can use the `example` option to select a example to build:
```
# build the `test` example
dioxus build --exmaple test
```
dx build --exmaple test
```
+6 -6
View File
@@ -1,13 +1,13 @@
# Clean
`dioxus clean` will clear the build artifacts (the out_dir and the cargo cache)
`dx clean` will clear the build artifacts (the out_dir and the cargo cache)
```
dioxus-clean
dioxus-clean
Clean build artifacts
USAGE:
dioxus clean [OPTIONS]
dx clean [OPTIONS]
OPTIONS:
--bin [default: None]
@@ -16,12 +16,12 @@ OPTIONS:
# Example
```
dioxus clean
dx clean
```
# Specify workspace bin
You can add the `--bin` option to select which crate you want Dioxus to clean artifacts from:
```
dioxus clean --bin app
```
dx clean --bin app
```
+10 -10
View File
@@ -1,13 +1,13 @@
# Serve
The `dioxus serve` can start a dev server with hot-reloading
The `dx serve` can start a dev server with hot-reloading
```
dioxus-serve
dioxus-serve
Build, watch & serve the Rust WASM app and all of its assets
USAGE:
dioxus serve [OPTIONS]
dx serve [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
@@ -20,7 +20,7 @@ OPTIONS:
You can use this command to build project and start a dev server:
```
dioxus serve
dx serve
```
## Serve Example
@@ -29,7 +29,7 @@ You can use the `example` option to serve a example:
```
# serve the `test` example
dioxus serve --exmaple test
dx serve --exmaple test
```
## Specify workspace bin
@@ -37,7 +37,7 @@ dioxus serve --exmaple test
You can add the `--bin` option to select which crate you want Dioxus to build and serve:
```
dioxus serve --bin app
dx serve --bin app
```
## Open Browser
@@ -45,7 +45,7 @@ dioxus serve --bin app
You can add the `--open` option to open system default browser when server startup:
```
dioxus serve --open
dx serve --open
```
## RSX Hot Reloading
@@ -53,7 +53,7 @@ dioxus serve --open
You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile:
```
dioxus serve --open
dx serve --open
```
## Cross Origin Policy
@@ -66,5 +66,5 @@ You can add the `cross-origin-policy` option to change cross-origin header to:
```
```
dioxus serve --corss-origin-policy
```
dx serve --corss-origin-policy
```
+7 -7
View File
@@ -1,13 +1,13 @@
# Translate
`dioxus translate` can translate some `html` file into a Dioxus compoent
`dx translate` can translate some `html` file into a Dioxus compoent
```
dioxus-translate
dioxus-translate
Translate some source file into a Dioxus component
USAGE:
dioxus translate [OPTIONS] [OUTPUT]
dx translate [OPTIONS] [OUTPUT]
ARGS:
<OUTPUT> Output file, defaults to stdout if not present
@@ -22,7 +22,7 @@ OPTIONS:
You can use the `file` option to set path to the `html` file to translate:
```
dioxus transtale --file ./index.html
dx transtale --file ./index.html
```
## Output rsx to a file
@@ -30,7 +30,7 @@ dioxus transtale --file ./index.html
You can pass a file to the traslate command to set the path to write the output of the command to:
```
dioxus translate --file ./index.html component.rsx
dx translate --file ./index.html component.rsx
```
## Output rsx to a file
@@ -38,7 +38,7 @@ dioxus translate --file ./index.html component.rsx
Setting the `component` option will create a compoent from the HTML:
```
dioxus translate --file ./index.html --component
dx translate --file ./index.html --component
```
## Example
@@ -65,4 +65,4 @@ fn component(cx: Scope) -> Element {
}
})
}
```
```
+2 -2
View File
@@ -29,8 +29,8 @@ General application confiration:
# default: web
default_platform = "web"
```
if you change this to `desktop`, the `dioxus build` will default building a desktop app
3. ***out_dir*** - The directory to place the build artifacts from `dioxus build` or `dioxus service` into. This is also where the `assets` directory will be copied to
if you change this to `desktop`, the `dx build` will default building a desktop app
3. ***out_dir*** - The directory to place the build artifacts from `dx build` or `dx service` into. This is also where the `assets` directory will be copied to
```
out_dir = "dist"
```
+4 -4
View File
@@ -4,10 +4,10 @@ Once you have the Dioxus CLI tool installed, you can use it to create dioxus pro
## Initializing a default project
First, run the `dioxus create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
First, run the `dx create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
```
dioxus create hello-dioxus
dx create hello-dioxus
```
> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template)
@@ -15,7 +15,7 @@ dioxus create hello-dioxus
>
> You can choose to create your project from a different template by passing the `template` argument:
> ```
> dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> ```
Next, move the current directory into your new project:
@@ -33,7 +33,7 @@ cd hello-dioxus
Finally, create serve your project with the Dioxus CLI:
```
dioxus serve
dx serve
```
By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)
+2 -2
View File
@@ -2,7 +2,7 @@
> For Cli 0.2.0 we will add `plugin-develop` support.
Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
Before the 0.2.0 we use `dx tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
### Why Lua ?
@@ -76,4 +76,4 @@ end
manager.serve.interval = 1000
return manager
```
```
+1 -1
View File
@@ -4,7 +4,7 @@
name = "{{project-name}}"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# you can also use `dx serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "{{default-platform}}"
+2 -2
View File
@@ -160,7 +160,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
}
} else {
log::warn!(
"Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
"Binaryen tool not found, you can use `dx tool add binaryen` to install it."
);
}
}
@@ -199,7 +199,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
}
} else {
log::warn!(
"Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it."
"Tailwind tool not found, you can use `dx tool add tailwindcss` to install it."
);
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ impl Plugin {
}
}
Plugin::Add { name: _ } => {
log::info!("You can use `dioxus plugin app-path` to get Installation position");
log::info!("You can use `dx plugin app-path` to get Installation position");
}
}
Ok(())
+1
View File
@@ -40,6 +40,7 @@ tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
pretty_assertions = "1.3.0"
rand = "0.8.5"
dioxus-ssr = { workspace = true }
[features]
default = []
+10 -19
View File
@@ -1,11 +1,10 @@
use std::{marker::PhantomData, panic::AssertUnwindSafe};
use crate::{
innerlude::Scoped,
nodes::{ComponentReturn, RenderReturn},
nodes::RenderReturn,
scopes::{Scope, ScopeState},
Element,
};
use std::panic::AssertUnwindSafe;
/// A trait that essentially allows VComponentProps to be used generically
///
@@ -18,19 +17,15 @@ pub(crate) unsafe trait AnyProps<'a> {
unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
}
pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> {
pub render_fn: fn(Scope<'a, P>) -> F,
pub(crate) struct VProps<'a, P> {
pub render_fn: fn(Scope<'a, P>) -> Element<'a>,
pub memo: unsafe fn(&P, &P) -> bool,
pub props: P,
_marker: PhantomData<A>,
}
impl<'a, P, A, F> VProps<'a, P, A, F>
where
F: ComponentReturn<'a, A>,
{
impl<'a, P> VProps<'a, P> {
pub(crate) fn new(
render_fn: fn(Scope<'a, P>) -> F,
render_fn: fn(Scope<'a, P>) -> Element<'a>,
memo: unsafe fn(&P, &P) -> bool,
props: P,
) -> Self {
@@ -38,15 +33,11 @@ where
render_fn,
memo,
props,
_marker: PhantomData,
}
}
}
unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F>
where
F: ComponentReturn<'a, A>,
{
unsafe impl<'a, P> AnyProps<'a> for VProps<'a, P> {
fn props_ptr(&self) -> *const () {
&self.props as *const _ as *const ()
}
@@ -69,12 +60,12 @@ where
scope: cx,
});
(self.render_fn)(scope).into_return(cx)
(self.render_fn)(scope)
}));
match res {
Ok(e) => e,
Err(_) => RenderReturn::default(),
Ok(Some(e)) => RenderReturn::Ready(e),
_ => RenderReturn::default(),
}
}
}
+6 -81
View File
@@ -5,10 +5,9 @@ use crate::mutations::Mutation::*;
use crate::nodes::VNode;
use crate::nodes::{DynamicNode, TemplateNode};
use crate::virtual_dom::VirtualDom;
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template};
use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, Template};
use std::cell::Cell;
use std::iter::Peekable;
use std::rc::Rc;
use TemplateNode::*;
#[cfg(debug_assertions)]
@@ -89,8 +88,7 @@ impl<'b> VirtualDom {
}
// Intialize the root nodes slice
node.root_ids
.intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice());
*node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()];
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
@@ -329,7 +327,7 @@ impl<'b> VirtualDom {
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
let this_id = self.next_root(template, root_idx);
template.root_ids.set(root_idx, this_id);
template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate {
name: template.template.get().name,
@@ -445,7 +443,7 @@ impl<'b> VirtualDom {
match node {
Text(text) => self.create_dynamic_text(template, text, idx),
Placeholder(place) => self.create_placeholder(place, template, idx),
Component(component) => self.create_component_node(template, component, idx),
Component(component) => self.create_component_node(template, component),
Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
}
}
@@ -502,7 +500,6 @@ impl<'b> VirtualDom {
&mut self,
template: &'b VNode<'b>,
component: &'b VComponent<'b>,
idx: usize,
) -> usize {
use RenderReturn::*;
@@ -512,9 +509,9 @@ impl<'b> VirtualDom {
component.scope.set(Some(scope));
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
Ready(t) => self.mount_component(scope, template, t, idx),
// Create the component's root element
Ready(t) => self.create_scope(scope, t),
Aborted(t) => self.mount_aborted(template, t),
Pending(_) => self.mount_async(template, idx, scope),
}
}
@@ -530,60 +527,6 @@ impl<'b> VirtualDom {
.unwrap_or_else(|| component.scope.get().unwrap())
}
fn mount_component(
&mut self,
scope: ScopeId,
parent: &'b VNode<'b>,
new: &'b VNode<'b>,
idx: usize,
) -> usize {
// Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
// is encountered
let mutations_to_this_point = self.mutations.edits.len();
// Create the component's root element
let created = self.create_scope(scope, new);
// If there are no suspense leaves below us, then just don't bother checking anything suspense related
if self.collected_leaves.is_empty() {
return created;
}
// If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
let boundary = match self.scopes[scope].has_context::<Rc<SuspenseContext>>() {
Some(boundary) => boundary,
_ => return created,
};
// Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree
let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
// Now connect everything to the boundary
self.scopes[scope].placeholder.set(Some(new_id));
// This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
// Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
let split_off = unsafe {
std::mem::transmute::<Vec<Mutation>, Vec<Mutation>>(
self.mutations.edits.split_off(mutations_to_this_point),
)
};
boundary.mutations.borrow_mut().edits.extend(split_off);
boundary.created_on_stack.set(created);
boundary
.waiting_on
.borrow_mut()
.extend(self.collected_leaves.drain(..));
// Now assign the placeholder in the DOM
self.mutations.push(AssignId {
id: new_id,
path: &parent.template.get().node_paths[idx][1..],
});
0
}
fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
let id = self.next_element(parent, &[]);
self.mutations.push(Mutation::CreatePlaceholder { id });
@@ -591,24 +534,6 @@ impl<'b> VirtualDom {
1
}
/// Take the rendered nodes from a component and handle them if they were async
///
/// IE simply assign an ID to the placeholder
fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize {
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
// Set the placeholder of the scope
self.scopes[scope].placeholder.set(Some(new_id));
// Since the placeholder is already in the DOM, we don't create any new nodes
self.mutations.push(AssignId {
id: new_id,
path: &template.template.get().node_paths[idx][1..],
});
0
}
fn set_slot(
&mut self,
template: &'b VNode<'b>,
+17 -37
View File
@@ -30,7 +30,7 @@ impl<'b> VirtualDom {
.try_load_node()
.expect("Call rebuild before diffing");
use RenderReturn::{Aborted, Pending, Ready};
use RenderReturn::{Aborted, Ready};
match (old, new) {
// Normal pathway
@@ -42,29 +42,14 @@ impl<'b> VirtualDom {
// Just move over the placeholder
(Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
// Becomes async, do nothing while we wait
(Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope),
// Placeholder becomes something
// We should also clear the error now
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
(Aborted(_), Pending(_)) => todo!("async should not resolve here"),
(Pending(_), Ready(_)) => todo!("async should not resolve here"),
(Pending(_), Aborted(_)) => todo!("async should not resolve here"),
(Pending(_), Pending(_)) => {
// All suspense should resolve before we diff it again
panic!("Should not roll from suspense to suspense.");
}
};
}
self.scope_stack.pop();
}
fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) {
//
}
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
let id = self.next_null();
p.id.set(Some(id));
@@ -139,18 +124,19 @@ impl<'b> VirtualDom {
.dynamic_nodes
.iter()
.zip(right_template.dynamic_nodes.iter())
.enumerate()
.for_each(|(idx, (left_node, right_node))| {
self.diff_dynamic_node(left_node, right_node, right_template, idx);
.for_each(|(left_node, right_node)| {
self.diff_dynamic_node(left_node, right_node, right_template);
});
// Make sure the roots get transferred over while we're here
right_template.root_ids.transfer(&left_template.root_ids);
*right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone();
let root_ids = right_template.root_ids.borrow();
// Update the node refs
for i in 0..right_template.root_ids.len() {
if let Some(root_id) = right_template.root_ids.get(i) {
self.update_template(root_id, right_template);
for i in 0..root_ids.len() {
if let Some(root_id) = root_ids.get(i) {
self.update_template(*root_id, right_template);
}
}
}
@@ -160,13 +146,12 @@ impl<'b> VirtualDom {
left_node: &'b DynamicNode<'b>,
right_node: &'b DynamicNode<'b>,
node: &'b VNode<'b>,
idx: usize,
) {
match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right, node),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx),
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
@@ -190,7 +175,6 @@ impl<'b> VirtualDom {
left: &'b VComponent<'b>,
right: &'b VComponent<'b>,
right_template: &'b VNode<'b>,
idx: usize,
) {
if std::ptr::eq(left, right) {
return;
@@ -198,7 +182,7 @@ impl<'b> VirtualDom {
// Replace components that have different render fns
if left.render_fn != right.render_fn {
return self.replace_vcomponent(right_template, right, idx, left);
return self.replace_vcomponent(right_template, right, left);
}
// Make sure the new vcomponent has the right scopeid associated to it
@@ -235,10 +219,9 @@ impl<'b> VirtualDom {
&mut self,
right_template: &'b VNode<'b>,
right: &'b VComponent<'b>,
idx: usize,
left: &'b VComponent<'b>,
) {
let m = self.create_component_node(right_template, right, idx);
let m = self.create_component_node(right_template, right);
let pre_edits = self.mutations.edits.len();
@@ -297,8 +280,7 @@ impl<'b> VirtualDom {
None => self.replace(left, [right]),
Some(components) => components
.into_iter()
.enumerate()
.for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)),
.for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
}
}
@@ -706,7 +688,7 @@ impl<'b> VirtualDom {
Some(node) => node,
None => {
self.mutations.push(Mutation::PushRoot {
id: node.root_ids.get(idx).unwrap(),
id: node.root_ids.borrow()[idx],
});
return 1;
}
@@ -735,7 +717,6 @@ impl<'b> VirtualDom {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
RenderReturn::Aborted(_node) => todo!(),
_ => todo!(),
}
}
}
@@ -842,7 +823,7 @@ impl<'b> VirtualDom {
if let Some(dy) = node.dynamic_root(idx) {
self.remove_dynamic_node(dy, gen_muts);
} else {
let id = node.root_ids.get(idx).unwrap();
let id = node.root_ids.borrow()[idx];
if gen_muts {
self.mutations.push(Mutation::Remove { id });
}
@@ -937,7 +918,6 @@ impl<'b> VirtualDom {
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
_ => todo!(),
};
// Restore the props back to the vcomponent in case it gets rendered again
@@ -950,7 +930,7 @@ impl<'b> VirtualDom {
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(0) {
None => node.root_ids.get(0).unwrap(),
None => node.root_ids.borrow()[0],
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_first_element(&t[0]),
Some(Placeholder(t)) => t.id.get().unwrap(),
@@ -966,7 +946,7 @@ impl<'b> VirtualDom {
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(node.template.get().roots.len() - 1) {
None => node.root_ids.last().unwrap(),
None => *node.root_ids.borrow().last().unwrap(),
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
Some(Placeholder(t)) => t.id.get().unwrap(),
+2 -3
View File
@@ -72,9 +72,8 @@ pub(crate) mod innerlude {
pub use crate::innerlude::{
fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError,
Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation,
Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext,
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
VirtualDom,
Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
};
/// The purpose of this module is to alleviate imports of many common types
+3 -132
View File
@@ -5,9 +5,8 @@ use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell, UnsafeCell},
cell::{Cell, RefCell},
fmt::{Arguments, Debug},
future::Future,
};
pub type TemplateId = &'static str;
@@ -28,9 +27,6 @@ pub enum RenderReturn<'a> {
/// In its place we've produced a placeholder to locate its spot in the dom when
/// it recovers.
Aborted(VPlaceholder),
/// An ongoing future that will resolve to a [`Element`]
Pending(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
}
impl<'a> Default for RenderReturn<'a> {
@@ -58,7 +54,7 @@ pub struct VNode<'a> {
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
/// the actual Dom
pub root_ids: BoxedCellSlice,
pub root_ids: RefCell<Vec<ElementId>>,
/// The dynamic parts of the template
pub dynamic_nodes: &'a [DynamicNode<'a>],
@@ -67,112 +63,13 @@ pub struct VNode<'a> {
pub dynamic_attrs: &'a [Attribute<'a>],
}
// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell)
#[derive(Debug, Default)]
pub struct BoxedCellSlice(UnsafeCell<Option<Box<[ElementId]>>>);
impl Clone for BoxedCellSlice {
fn clone(&self) -> Self {
Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() }))
}
}
impl BoxedCellSlice {
pub fn last(&self) -> Option<ElementId> {
unsafe {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().last().copied())
}
}
pub fn get(&self, idx: usize) -> Option<ElementId> {
unsafe {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().get(idx).copied())
}
}
pub unsafe fn get_unchecked(&self, idx: usize) -> Option<ElementId> {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().get(idx).copied())
}
pub fn set(&self, idx: usize, new: ElementId) {
unsafe {
if let Some(inner) = &mut *self.0.get() {
inner[idx] = new;
}
}
}
pub fn intialize(&self, contents: Box<[ElementId]>) {
unsafe {
*self.0.get() = Some(contents);
}
}
pub fn transfer(&self, other: &Self) {
unsafe {
*self.0.get() = (*other.0.get()).clone();
}
}
pub fn take_from(&self, other: &Self) {
unsafe {
*self.0.get() = (*other.0.get()).take();
}
}
pub fn len(&self) -> usize {
unsafe {
(*self.0.get())
.as_ref()
.map(|inner| inner.len())
.unwrap_or(0)
}
}
}
impl<'a> IntoIterator for &'a BoxedCellSlice {
type Item = ElementId;
type IntoIter = BoxedCellSliceIter<'a>;
fn into_iter(self) -> Self::IntoIter {
BoxedCellSliceIter {
index: 0,
borrow: self,
}
}
}
pub struct BoxedCellSliceIter<'a> {
index: usize,
borrow: &'a BoxedCellSlice,
}
impl Iterator for BoxedCellSliceIter<'_> {
type Item = ElementId;
fn next(&mut self) -> Option<Self::Item> {
let result = self.borrow.get(self.index);
if result.is_some() {
self.index += 1;
}
result
}
}
impl<'a> VNode<'a> {
/// Create a template with no nodes that will be skipped over during diffing
pub fn empty() -> Element<'a> {
Some(VNode {
key: None,
parent: None,
root_ids: BoxedCellSlice::default(),
root_ids: Default::default(),
dynamic_nodes: &[],
dynamic_attrs: &[],
template: Cell::new(Template {
@@ -688,32 +585,6 @@ impl<T: Any + PartialEq + 'static> AnyValue for T {
}
}
#[doc(hidden)]
pub trait ComponentReturn<'a, A = ()> {
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>;
}
impl<'a> ComponentReturn<'a> for Element<'a> {
fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
match self {
Some(node) => RenderReturn::Ready(node),
None => RenderReturn::default(),
}
}
}
#[doc(hidden)]
pub struct AsyncMarker;
impl<'a, F> ComponentReturn<'a, AsyncMarker> for F
where
F: Future<Output = Element<'a>> + 'a,
{
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
RenderReturn::Pending(unsafe { BumpBox::from_raw(f) })
}
}
impl<'a> RenderReturn<'a> {
pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> {
unsafe { std::mem::transmute(self) }
+1 -1
View File
@@ -70,6 +70,6 @@ impl EmptyBuilder {
/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
/// to initialize a component's props.
pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder {
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
T::builder()
}
-9
View File
@@ -1,11 +1,9 @@
use crate::ScopeId;
use slab::Slab;
mod suspense;
mod task;
mod wait;
pub use suspense::*;
pub use task::*;
/// The type of message that can be sent to the scheduler.
@@ -18,9 +16,6 @@ pub(crate) enum SchedulerMsg {
/// A task has woken and needs to be progressed
TaskNotified(TaskId),
/// A task has woken and needs to be progressed
SuspenseNotified(SuspenseId),
}
use std::{cell::RefCell, rc::Rc};
@@ -30,9 +25,6 @@ pub(crate) struct Scheduler {
/// Tasks created with cx.spawn
pub tasks: RefCell<Slab<LocalTask>>,
/// Async components
pub leaves: RefCell<Slab<SuspenseLeaf>>,
}
impl Scheduler {
@@ -40,7 +32,6 @@ impl Scheduler {
Rc::new(Scheduler {
sender,
tasks: RefCell::new(Slab::new()),
leaves: RefCell::new(Slab::new()),
})
}
}
+3 -29
View File
@@ -11,17 +11,10 @@ use std::{
collections::HashSet,
};
/// An ID representing an ongoing suspended component
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct SuspenseId(pub usize);
/// A boundary in the VirtualDom that captures all suspended components below it
pub struct SuspenseContext {
pub(crate) id: ScopeId,
pub(crate) waiting_on: RefCell<HashSet<SuspenseId>>,
pub(crate) mutations: RefCell<Mutations<'static>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
pub(crate) created_on_stack: Cell<usize>,
pub(crate) waiting_on: RefCell<HashSet<ScopeId>>,
}
impl SuspenseContext {
@@ -30,29 +23,10 @@ impl SuspenseContext {
Self {
id,
waiting_on: Default::default(),
mutations: RefCell::new(Mutations::default()),
placeholder: Cell::new(None),
created_on_stack: Cell::new(0),
}
}
}
pub(crate) struct SuspenseLeaf {
pub(crate) scope_id: ScopeId,
pub(crate) notified: Cell<bool>,
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
pub(crate) waker: Waker,
}
pub struct SuspenseHandle {
pub(crate) id: SuspenseId,
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
}
impl ArcWake for SuspenseHandle {
fn wake_by_ref(arc_self: &Arc<Self>) {
_ = arc_self
.tx
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
pub fn mark_suspend(&self, id: ScopeId) {
self.waiting_on.borrow_mut().insert(id);
}
}
+2 -83
View File
@@ -1,16 +1,5 @@
use futures_util::FutureExt;
use std::{
rc::Rc,
task::{Context, Poll},
};
use crate::{
innerlude::{Mutation, Mutations, SuspenseContext},
nodes::RenderReturn,
ScopeId, TaskId, VNode, VirtualDom,
};
use super::SuspenseId;
use crate::{TaskId, VirtualDom};
use std::task::Context;
impl VirtualDom {
/// Handle notifications by tasks inside the scheduler
@@ -38,74 +27,4 @@ impl VirtualDom {
tasks.try_remove(id.0);
}
}
pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
self.scopes[id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
}
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
let leaves = self.scheduler.leaves.borrow_mut();
let leaf = leaves.get(id.0).unwrap();
let scope_id = leaf.scope_id;
// todo: cache the waker
let mut cx = Context::from_waker(&leaf.waker);
// Safety: the future is always pinned to the bump arena
let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
let as_pinned_mut = &mut pinned;
// the component finished rendering and gave us nodes
// we should attach them to that component and then render its children
// continue rendering the tree until we hit yet another suspended component
if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
let fiber = self.acquire_suspense_boundary(leaf.scope_id);
let scope = &self.scopes[scope_id];
let arena = scope.current_frame();
let ret = arena.bump().alloc(match new_nodes {
Some(new) => RenderReturn::Ready(new),
None => RenderReturn::default(),
});
arena.node.set(ret);
fiber.waiting_on.borrow_mut().remove(&id);
if let RenderReturn::Ready(template) = ret {
let mutations_ref = &mut fiber.mutations.borrow_mut();
let mutations = &mut **mutations_ref;
let template: &VNode = unsafe { std::mem::transmute(template) };
let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
std::mem::swap(&mut self.mutations, mutations);
let place_holder_id = scope.placeholder.get().unwrap();
self.scope_stack.push(scope_id);
drop(leaves);
let created = self.create(template);
self.scope_stack.pop();
mutations.push(Mutation::ReplaceWith {
id: place_holder_id,
m: created,
});
for leaf in self.collected_leaves.drain(..) {
fiber.waiting_on.borrow_mut().insert(leaf);
}
std::mem::swap(&mut self.mutations, mutations);
if fiber.waiting_on.borrow().is_empty() {
self.finished_fibers.push(fiber.id);
}
}
}
}
}
+12 -65
View File
@@ -2,18 +2,10 @@ use crate::{
any_props::AnyProps,
bump_frame::BumpFrame,
innerlude::DirtyScope,
innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf},
nodes::RenderReturn,
scopes::{ScopeId, ScopeState},
virtual_dom::VirtualDom,
};
use futures_util::FutureExt;
use std::{
mem,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
impl VirtualDom {
pub(super) fn new_scope(
@@ -33,10 +25,10 @@ impl VirtualDom {
name,
props: Some(props),
tasks: self.scheduler.clone(),
placeholder: Default::default(),
node_arena_1: BumpFrame::new(0),
node_arena_2: BumpFrame::new(0),
spawned_tasks: Default::default(),
suspended: Default::default(),
render_cnt: Default::default(),
hooks: Default::default(),
hook_idx: Default::default(),
@@ -58,74 +50,21 @@ impl VirtualDom {
// Remove all the outdated listeners
self.ensure_drop_safety(scope_id);
let mut new_nodes = unsafe {
let new_nodes = unsafe {
self.scopes[scope_id].previous_frame().bump_mut().reset();
let scope = &self.scopes[scope_id];
scope.suspended.set(false);
scope.hook_idx.set(0);
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref();
let props: &dyn AnyProps = mem::transmute(props);
let props: &dyn AnyProps = std::mem::transmute(props);
props.render(scope).extend_lifetime()
};
// immediately resolve futures that can be resolved
if let RenderReturn::Pending(task) = &mut new_nodes {
let mut leaves = self.scheduler.leaves.borrow_mut();
let entry = leaves.vacant_entry();
let suspense_id = SuspenseId(entry.key());
let leaf = SuspenseLeaf {
scope_id,
task: task.as_mut(),
notified: Default::default(),
waker: futures_util::task::waker(Arc::new(SuspenseHandle {
id: suspense_id,
tx: self.scheduler.sender.clone(),
})),
};
let mut cx = Context::from_waker(&leaf.waker);
// safety: the task is already pinned in the bump arena
let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
// Keep polling until either we get a value or the future is not ready
loop {
match pinned.poll_unpin(&mut cx) {
// If nodes are produced, then set it and we can break
Poll::Ready(nodes) => {
new_nodes = match nodes {
Some(nodes) => RenderReturn::Ready(nodes),
None => RenderReturn::default(),
};
break;
}
// If no nodes are produced but the future woke up immediately, then try polling it again
// This circumvents things like yield_now, but is important is important when rendering
// components that are just a stream of immediately ready futures
_ if leaf.notified.get() => {
leaf.notified.set(false);
continue;
}
// If no nodes are produced, then we need to wait for the future to be woken up
// Insert the future into fiber leaves and break
_ => {
entry.insert(leaf);
self.collected_leaves.push(suspense_id);
break;
}
};
}
};
let scope = &self.scopes[scope_id];
// We write on top of the previous frame and then make it the current by pushing the generation forward
@@ -144,6 +83,14 @@ impl VirtualDom {
id: scope.id,
});
if scope.suspended.get() {
if matches!(allocated, RenderReturn::Aborted(_)) {
self.suspended_scopes.insert(scope.id);
}
} else if !self.suspended_scopes.is_empty() {
_ = self.suspended_scopes.remove(&scope.id);
}
// rebind the lifetime now that its stored internally
unsafe { allocated.extend_lifetime_ref() }
}
+10 -5
View File
@@ -1,12 +1,11 @@
use crate::{
any_props::AnyProps,
any_props::VProps,
arena::ElementId,
bump_frame::BumpFrame,
innerlude::{DynamicNode, EventHandler, VComponent, VText},
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
lazynodes::LazyNodes,
nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
@@ -169,6 +168,7 @@ pub struct ScopeState {
pub(crate) id: ScopeId,
pub(crate) height: u32,
pub(crate) suspended: Cell<bool>,
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
pub(crate) hook_idx: Cell<usize>,
@@ -182,7 +182,6 @@ pub struct ScopeState {
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
}
impl<'src> ScopeState {
@@ -574,9 +573,9 @@ impl<'src> ScopeState {
/// fn(Scope<Props>) -> Element;
/// async fn(Scope<Props<'_>>) -> Element;
/// ```
pub fn component<P, A, F: ComponentReturn<'src, A>>(
pub fn component<P>(
&'src self,
component: fn(Scope<'src, P>) -> F,
component: fn(Scope<'src, P>) -> Element<'src>,
props: P,
fn_name: &'static str,
) -> DynamicNode<'src>
@@ -655,6 +654,12 @@ impl<'src> ScopeState {
None
}
/// Mark this component as suspended and then return None
pub fn suspend(&self) -> Option<Element> {
self.suspended.set(true);
None
}
/// Store a value between renders. The foundational hook for all other hooks.
///
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.
+25 -95
View File
@@ -9,14 +9,13 @@ use crate::{
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
scheduler::SuspenseId,
scopes::{ScopeId, ScopeState},
AttributeValue, Element, Event, Scope, SuspenseContext,
AttributeValue, Element, Event, Scope,
};
use futures_util::{pin_mut, StreamExt};
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
/// A virtual node system that progresses user events and diffs UI trees.
///
@@ -186,11 +185,9 @@ pub struct VirtualDom {
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
pub(crate) scope_stack: Vec<ScopeId>,
pub(crate) collected_leaves: Vec<SuspenseId>,
// Whenever a suspense tree is finished, we push its boundary onto this stack.
// When "render_with_deadline" is called, we pop the stack and return the mutations
pub(crate) finished_fibers: Vec<ScopeId>,
// Currently suspended scopes
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
@@ -262,8 +259,7 @@ impl VirtualDom {
elements: Default::default(),
scope_stack: Vec::new(),
dirty_scopes: BTreeSet::new(),
collected_leaves: Vec::new(),
finished_fibers: Vec::new(),
suspended_scopes: FxHashSet::default(),
mutations: Mutations::default(),
};
@@ -272,12 +268,6 @@ impl VirtualDom {
"app",
);
// The root component is always a suspense boundary for any async children
// This could be unexpected, so we might rethink this behavior later
//
// We *could* just panic if the suspense boundary is not found
root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0))));
// Unlike react, we provide a default error boundary that just renders the error as a string
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
@@ -319,25 +309,6 @@ impl VirtualDom {
}
}
/// Determine whether or not a scope is currently in a suspended state
///
/// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
/// currently suspended.
pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
!self.scopes[id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap()
.waiting_on
.borrow()
.is_empty()
}
/// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
/// ready to be rendered.
pub fn has_suspended_work(&self) -> bool {
!self.scheduler.leaves.borrow().is_empty()
}
/// Call a listener inside the VirtualDom with data from outside the VirtualDom.
///
/// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
@@ -485,7 +456,6 @@ impl VirtualDom {
Some(msg) => match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
},
// If they're not ready, then we should wait for them to be ready
@@ -495,7 +465,7 @@ impl VirtualDom {
Ok(None) => return,
Err(_) => {
// If we have any dirty scopes, or finished fiber trees then we should exit
if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() {
if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
return;
}
@@ -513,7 +483,6 @@ impl VirtualDom {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
}
}
}
@@ -574,7 +543,6 @@ impl VirtualDom {
}
// If an error occurs, we should try to render the default error component and context where the error occured
RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"),
RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"),
}
self.finalize()
@@ -598,6 +566,24 @@ impl VirtualDom {
}
}
/// Render the virtual dom, waiting for all suspense to be finished
///
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
pub async fn wait_for_suspense(&mut self) {
loop {
// println!("waiting for suspense {:?}", self.suspended_scopes);
if self.suspended_scopes.is_empty() {
return;
}
// println!("waiting for suspense");
self.wait_for_work().await;
// println!("Rendered immediately");
_ = self.render_immediate();
}
}
/// Render what you can given the timeline and then move on
///
/// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
@@ -609,26 +595,6 @@ impl VirtualDom {
self.process_events();
loop {
// first, unload any complete suspense trees
for finished_fiber in self.finished_fibers.drain(..) {
let scope = &self.scopes[finished_fiber];
let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
self.mutations
.templates
.append(&mut context.mutations.borrow_mut().templates);
self.mutations
.edits
.append(&mut context.mutations.borrow_mut().edits);
// TODO: count how many nodes are on the stack?
self.mutations.push(Mutation::ReplaceWith {
id: context.placeholder.get().unwrap(),
m: 1,
})
}
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
@@ -639,40 +605,9 @@ impl VirtualDom {
continue;
}
// if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update
if self.is_scope_suspended(dirty.id) {
continue;
}
// Save the current mutations length so we can split them into boundary
let mutations_to_this_point = self.mutations.edits.len();
// Run the scope and get the mutations
self.run_scope(dirty.id);
self.diff_scope(dirty.id);
// If suspended leaves are present, then we should find the boundary for this scope and attach things
// No placeholder necessary since this is a diff
if !self.collected_leaves.is_empty() {
let mut boundary = self.scopes[dirty.id]
.consume_context::<Rc<SuspenseContext>>()
.unwrap();
let boundary_mut = boundary.borrow_mut();
// Attach mutations
boundary_mut
.mutations
.borrow_mut()
.edits
.extend(self.mutations.edits.split_off(mutations_to_this_point));
// Attach suspended leaves
boundary
.waiting_on
.borrow_mut()
.extend(self.collected_leaves.drain(..));
}
}
// If there's more work, then just continue, plenty of work to do
@@ -680,11 +615,6 @@ impl VirtualDom {
continue;
}
// If there's no pending suspense, then we have no reason to wait for anything
if self.scheduler.leaves.borrow().is_empty() {
return self.finalize();
}
// Poll the suspense leaves in the meantime
let mut work = self.wait_for_work();
-7
View File
@@ -1,9 +1,6 @@
//! Tests related to safety of the library.
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_core::SuspenseContext;
/// Ensure no issues with not calling rebuild
#[test]
@@ -17,8 +14,4 @@ fn root_node_isnt_null() {
// The height should be 0
assert_eq!(scope.height(), 0);
// There should be a default suspense context
// todo: there should also be a default error boundary
assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
}
+20 -82
View File
@@ -1,100 +1,38 @@
use dioxus::core::ElementId;
use dioxus::core::{Mutation::*, SuspenseContext};
use dioxus::prelude::*;
use std::future::IntoFuture;
use std::rc::Rc;
use std::time::Duration;
#[test]
fn it_works() {
#[tokio::test]
async fn it_works() {
// wait just a moment, not enough time for the boundary to resolve
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.wait_for_suspense().await;
let out = dioxus_ssr::pre_render(&dom);
{
let mutations = dom.rebuild().santize();
assert_eq!(out, "<div>Waiting for... child</div>");
// We should at least get the top-level template in before pausing for the children
// note: we dont test template edits anymore
// assert_eq!(
// mutations.templates,
// [
// CreateElement { name: "div" },
// CreateStaticText { value: "Waiting for child..." },
// CreateStaticPlaceholder,
// AppendChildren { m: 2 },
// SaveTemplate { name: "template", m: 1 }
// ]
// );
// And we should load it in and assign the placeholder properly
assert_eq!(
mutations.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
// hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
// can we even?
AssignId { path: &[1], id: ElementId(3) },
AppendChildren { m: 1, id: ElementId(0) },
]
);
}
dom.wait_for_work().await;
});
dbg!(out);
}
fn app(cx: Scope) -> Element {
cx.render(rsx!(
div {
"Waiting for child..."
suspense_boundary {}
"Waiting for... "
suspended_child {}
}
))
}
fn suspense_boundary(cx: Scope) -> Element {
cx.use_hook(|| {
cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
});
fn suspended_child(cx: Scope) -> Element {
let val = use_state(cx, || 0);
// Ensure the right types are found
cx.has_context::<Rc<SuspenseContext>>().unwrap();
if **val < 3 {
let mut val = val.clone();
cx.spawn(async move {
val += 1;
});
return cx.suspend()?;
}
cx.render(rsx!(async_child {}))
}
async fn async_child(cx: Scope<'_>) -> Element {
use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await;
cx.render(rsx!(async_text {}))
}
async fn async_text(cx: Scope<'_>) -> Element {
let username = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
"async child 1"
});
let age = use_future!(cx, || async {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1234
});
let (_user, _age) = use_future!(cx, || async {
tokio::join!(
tokio::time::sleep(std::time::Duration::from_secs(1)),
tokio::time::sleep(std::time::Duration::from_secs(2))
);
("async child 1", 1234)
})
.await;
let (username, age) = tokio::join!(username.into_future(), age.into_future());
cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
render!("child")
}
+8 -5
View File
@@ -1,9 +1,9 @@
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
use dioxus::prelude::*;
use std::time::Duration;
use std::{sync::atomic::AtomicUsize, time::Duration};
static mut POLL_COUNT: usize = 0;
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[tokio::test]
async fn it_works() {
@@ -18,7 +18,10 @@ async fn it_works() {
// By the time the tasks are finished, we should've accumulated ticks from two tasks
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
assert_eq!(unsafe { POLL_COUNT }, 135);
assert_eq!(
POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
135
);
}
fn app(cx: Scope) -> Element {
@@ -26,14 +29,14 @@ fn app(cx: Scope) -> Element {
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(50)).await;
unsafe { POLL_COUNT += x }
POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed);
}
});
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(25)).await;
unsafe { POLL_COUNT += x * 2 }
POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed);
}
});
});
+42 -22
View File
@@ -8,11 +8,48 @@ pub(crate) struct FileDialogRequest {
#[serde(default)]
accept: Option<String>,
multiple: bool,
directory: bool,
pub event: String,
pub target: usize,
pub bubbles: bool,
}
fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
dialog.pick_folder().into_iter().collect()
}
}
fn get_file_event_for_file(
request: &FileDialogRequest,
mut dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
} else {
dialog.pick_file().into_iter().collect()
};
files
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
@@ -36,30 +73,13 @@ pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
target_os = "openbsd"
))]
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
let mut dialog = rfd::FileDialog::new();
let dialog = rfd::FileDialog::new();
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
if request.directory {
get_file_event_for_folder(request, dialog)
} else {
dialog.pick_file().into_iter().collect()
};
files
get_file_event_for_file(request, dialog)
}
}
enum Filters {
+1 -1
View File
@@ -24,7 +24,7 @@ fn module_loader(root_name: &str) -> String {
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
+1 -1
View File
@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| {
if let RenderReturn::Ready(node) = cx.root_node() {
if let Some(id) = node.root_ids.get(0){
if let Some(id) = node.root_ids.borrow().get(0).cloned() {
let node = tui_query.get(mapping.get_node_id(id).unwrap());
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();
+23 -10
View File
@@ -10,28 +10,33 @@ keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"]
[dependencies]
# server functions
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "15a4e54435eb5a539afb75891292bcccd2cc8e85", default-features = false, features = ["stable"] }
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "6b90e1babd425c9a324181c86e3fd1b942c9b10f", default-features = false }
dioxus_server_macro = { path = "server-macro" }
# warp
warp = { version = "0.3.3", optional = true }
warp = { version = "0.3.5", features = ["compression-gzip"], optional = true }
http-body = { version = "0.4.5", optional = true }
http-body-util = "0.1.0-rc.2"
# axum
axum = { version = "0.6.1", features = ["ws"], optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs"] }
axum = { version = "0.6.1", features = ["ws", "macros"], optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] }
tower = { version = "0.4.13", features = ["util"], optional = true }
axum-macros = "0.3.7"
# salvo
salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] }
salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] }
serde = "1.0.159"
# Dioxus + SSR
dioxus-core = { workspace = true }
dioxus = { workspace = true }
dioxus-ssr = { workspace = true, optional = true }
hyper = { version = "0.14.25", optional = true }
http = { version = "0.2.9", optional = true }
# Router Intigration
dioxus-router = { workspace = true, optional = true }
log = { workspace = true }
once_cell = "1.17.1"
thiserror = "1.0.40"
@@ -43,9 +48,13 @@ serde_json = { version = "1.0.95", optional = true }
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
futures-util = { workspace = true, optional = true }
postcard = { version = "1.0.4", features = ["use-std"] }
yazi = "0.1.5"
base64 = "0.21.0"
pin-project = "1.1.2"
async-trait = "0.1.71"
bytes = "1.4.0"
tower-layer = "0.3.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { workspace = true }
@@ -54,10 +63,14 @@ web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "Ht
[features]
default = ["hot-reload", "default-tls"]
hot-reload = ["serde_json", "tokio-stream", "futures-util"]
warp = ["dep:warp", "http-body", "ssr"]
router = ["dioxus-router"]
hot-reload = ["serde_json", "futures-util"]
warp = ["dep:warp", "ssr"]
axum = ["dep:axum", "tower-http", "ssr"]
salvo = ["dep:salvo", "ssr"]
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"]
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "tower", "hyper", "http", "http-body", "dioxus-router/ssr", "tokio-stream"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
[dev-dependencies]
dioxus-fullstack = { path = ".", features = ["router"] }
@@ -0,0 +1,3 @@
dist
target
static
@@ -0,0 +1,53 @@
[package]
name = "axum-auth"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
execute = "0.2.12"
tower-http = { version = "0.4.1", features = ["auth"], optional = true }
simple_logger = { version = "4.2.0", optional = true }
async-trait = { version = "0.1.71", optional = true }
sqlx = { version = "0.7.0", features = [
"macros",
"migrate",
"postgres",
"sqlite",
"_unstable-all-types",
"tls-rustls",
"runtime-tokio",
], optional = true }
anyhow = "1.0.71"
http = { version = "0.2.9", optional = true }
tower = { version = "0.4.13", optional = true }
[dependencies.axum_session]
version = "0.3.0"
features = ["sqlite-rustls"]
optional = true
[dependencies.axum_session_auth]
version = "0.3.0"
features = ["sqlite-rustls"]
optional = true
[features]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "simple_logger", "async-trait", "sqlx", "axum_session", "axum_session_auth", "http", "tower"]
web = ["dioxus-web"]
[profile.release]
lto = true
panic = "abort"
opt-level = 'z'
strip = true
codegen-units = 1
@@ -0,0 +1,262 @@
use async_trait::async_trait;
use axum::{
http::Method,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionSqlitePool, SessionStore};
use axum_session_auth::*;
use core::pin::Pin;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use std::error::Error;
use std::future::Future;
use std::{collections::HashSet, net::SocketAddr, str::FromStr};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub anonymous: bool,
pub username: String,
pub permissions: HashSet<String>,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlPermissionTokens {
pub token: String,
}
impl Default for User {
fn default() -> Self {
let mut permissions = HashSet::new();
permissions.insert("Category::View".to_owned());
Self {
id: 1,
anonymous: true,
username: "Guest".into(),
permissions,
}
}
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
User::get_user(userid, pool)
.await
.ok_or_else(|| anyhow::anyhow!("Could not load user"))
}
fn is_authenticated(&self) -> bool {
!self.anonymous
}
fn is_active(&self) -> bool {
!self.anonymous
}
fn is_anonymous(&self) -> bool {
self.anonymous
}
}
#[async_trait]
impl HasPermission<SqlitePool> for User {
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
self.permissions.contains(perm)
}
}
impl User {
pub async fn get_user(id: i64, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = $1;",
)
.bind(id)
.fetch_all(pool)
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
}
pub async fn create_user_tables(pool: &SqlitePool) {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
"id" INTEGER PRIMARY KEY,
"anonymous" BOOLEAN NOT NULL,
"username" VARCHAR(256) NOT NULL
)
"#,
)
.execute(pool)
.await
.unwrap();
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS user_permissions (
"user_id" INTEGER NOT NULL,
"token" VARCHAR(256) NOT NULL
)
"#,
)
.execute(pool)
.await
.unwrap();
sqlx::query(
r#"
INSERT INTO users
(id, anonymous, username) SELECT 1, true, 'Guest'
ON CONFLICT(id) DO UPDATE SET
anonymous = EXCLUDED.anonymous,
username = EXCLUDED.username
"#,
)
.execute(pool)
.await
.unwrap();
sqlx::query(
r#"
INSERT INTO users
(id, anonymous, username) SELECT 2, false, 'Test'
ON CONFLICT(id) DO UPDATE SET
anonymous = EXCLUDED.anonymous,
username = EXCLUDED.username
"#,
)
.execute(pool)
.await
.unwrap();
sqlx::query(
r#"
INSERT INTO user_permissions
(user_id, token) SELECT 2, 'Category::View'
"#,
)
.execute(pool)
.await
.unwrap();
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlUser {
pub id: i32,
pub anonymous: bool,
pub username: String,
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
User {
id: self.id,
anonymous: self.anonymous,
username: self.username,
permissions: if let Some(user_perms) = sql_user_perms {
user_perms
.into_iter()
.map(|x| x.token)
.collect::<HashSet<String>>()
} else {
HashSet::<String>::new()
},
}
}
}
pub async fn connect_to_database() -> SqlitePool {
let connect_opts = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
SqlitePoolOptions::new()
.max_connections(5)
.connect_with(connect_opts)
.await
.unwrap()
}
pub struct Session(
pub axum_session_auth::AuthSession<
crate::auth::User,
i64,
axum_session_auth::SessionSqlitePool,
sqlx::SqlitePool,
>,
);
impl std::ops::Deref for Session {
type Target = axum_session_auth::AuthSession<
crate::auth::User,
i64,
axum_session_auth::SessionSqlitePool,
sqlx::SqlitePool,
>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Session {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug)]
pub struct AuthSessionLayerNotFound;
impl std::fmt::Display for AuthSessionLayerNotFound {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AuthSessionLayer was not found")
}
}
impl std::error::Error for AuthSessionLayerNotFound {}
impl IntoResponse for AuthSessionLayerNotFound {
fn into_response(self) -> Response {
(
http::status::StatusCode::INTERNAL_SERVER_ERROR,
"AuthSessionLayer was not found",
)
.into_response()
}
}
#[async_trait]
impl<S: std::marker::Sync + std::marker::Send> axum::extract::FromRequestParts<S> for Session {
type Rejection = AuthSessionLayerNotFound;
async fn from_request_parts(
parts: &mut http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
axum_session_auth::AuthSession::<
crate::auth::User,
i64,
axum_session_auth::SessionSqlitePool,
sqlx::SqlitePool,
>::from_request_parts(parts, state)
.await
.map(Session)
.map_err(|_| AuthSessionLayerNotFound)
}
}
@@ -0,0 +1,164 @@
//! Run with:
//!
//! ```sh
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case, unused)]
#[cfg(feature = "ssr")]
mod auth;
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
// Hydrate the application on the client
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
#[cfg(feature = "ssr")]
{
use crate::auth::*;
use axum::routing::*;
use axum_session::SessionConfig;
use axum_session::SessionStore;
use axum_session_auth::AuthConfig;
use axum_session_auth::SessionSqlitePool;
simple_logger::SimpleLogger::new().init().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let pool = connect_to_database().await;
//This Defaults as normal Cookies.
//To enable Private cookies for integrity, and authenticity please check the next Example.
let session_config = SessionConfig::default().with_table_name("test_table");
let auth_config = AuthConfig::<i64>::default().with_anonymous_user_id(Some(1));
let session_store = SessionStore::<SessionSqlitePool>::new(
Some(pool.clone().into()),
session_config,
)
.await
.unwrap();
//Create the Database table for storing our Session Data.
session_store.initiate().await.unwrap();
User::create_user_tables(&pool).await;
// build our application with some routes
let app = Router::new()
// Server side render the application, serve static assets, and register server functions
.serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
.layer(
axum_session_auth::AuthSessionLayer::<
crate::auth::User,
i64,
axum_session_auth::SessionSqlitePool,
sqlx::SqlitePool,
>::new(Some(pool))
.with_config(auth_config),
)
.layer(axum_session::SessionLayer::new(session_store));
// run it
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
});
}
}
fn app(cx: Scope) -> Element {
let user_name = use_state(cx, || "?".to_string());
let permissions = use_state(cx, || "?".to_string());
cx.render(rsx! {
div {
button {
onclick: move |_| {
async move {
login().await.unwrap();
}
},
"Login Test User"
}
}
div {
button {
onclick: move |_| {
to_owned![user_name];
async move {
if let Ok(data) = get_user_name().await {
user_name.set(data);
}
}
},
"Get User Name"
}
"User name: {user_name}"
}
div {
button {
onclick: move |_| {
to_owned![permissions];
async move {
if let Ok(data) = get_permissions().await {
permissions.set(data);
}
}
},
"Get Permissions"
}
"Permissions: {permissions}"
}
})
}
#[server(GetUserName)]
pub async fn get_user_name() -> Result<String, ServerFnError> {
let session: crate::auth::Session = extract().await?;
Ok(session.0.current_user.unwrap().username.to_string())
}
#[server(Login)]
pub async fn login() -> Result<(), ServerFnError> {
let auth: crate::auth::Session = extract().await?;
auth.login_user(2);
Ok(())
}
#[server(Permissions)]
pub async fn get_permissions() -> Result<String, ServerFnError> {
let method: axum::http::Method = extract().await?;
let auth: crate::auth::Session = extract().await?;
let current_user = auth.current_user.clone().unwrap_or_default();
// lets check permissions only and not worry about if they are anon or not
if !axum_session_auth::Auth::<crate::auth::User, i64, sqlx::SqlitePool>::build(
[axum::http::Method::POST],
false,
)
.requires(axum_session_auth::Rights::any([
axum_session_auth::Rights::permission("Category::View"),
axum_session_auth::Rights::permission("Admin::View"),
]))
.validate(&current_user, &method, None)
.await
{
return Ok(format!(
"User {}, Does not have permissions needed to view this page please login",
current_user.username
));
}
Ok(format!(
"User has Permissions needed. Here are the Users permissions: {:?}",
current_user.permissions
))
}
@@ -1,6 +1,6 @@
// Run with:
// ```bash
// cargo run --bin client --features="desktop"
// cargo run --bin client --features desktop
// ```
use axum_desktop::*;
@@ -8,6 +8,6 @@ use dioxus_fullstack::prelude::server_fn::set_server_url;
fn main() {
// Set the url of the server where server functions are hosted.
set_server_url("http://localhost:8080");
set_server_url("http://127.0.0.0:8080");
dioxus_desktop::launch(app)
}
@@ -1,9 +1,8 @@
// Run with:
// ```bash
// cargo run --bin server --features="ssr"
// cargo run --bin server --features ssr
// ```
use axum_desktop::*;
use dioxus_fullstack::prelude::*;
#[tokio::main]
@@ -1,2 +1,3 @@
dist
target
target
static
@@ -7,15 +7,24 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
execute = "0.2.12"
tower-http = { version = "0.4.1", features = ["auth"] }
simple_logger = "4.2.0"
[features]
default = ["web"]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
web = ["dioxus-web"]
[profile.release]
lto = true
panic = "abort"
opt-level = 'z'
strip = true
codegen-units = 1
@@ -1,56 +1,15 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case)]
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use dioxus_fullstack::{launch, prelude::*};
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(),
)
.into_make_service(),
)
.await
.unwrap();
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
@@ -67,12 +26,11 @@ fn app(cx: Scope<AppProps>) -> Element {
button {
onclick: move |_| {
to_owned![text];
let sc = cx.sc();
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(sc, data).await.unwrap();
post_server_data(data).await.unwrap();
}
}
},
@@ -83,12 +41,10 @@ fn app(cx: Scope<AppProps>) -> Element {
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
let axum::extract::Host(host): axum::extract::Host = extract()?;
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
println!("{:?}", host);
Ok(())
}
@@ -97,3 +53,9 @@ async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), S
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
fn main() {
launch!(@([127, 0, 0, 1], 8080), app, {
serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }),
});
}
@@ -1,2 +1,3 @@
dist
target
target
static
@@ -7,18 +7,15 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-router = { workspace = true }
dioxus-fullstack = { workspace = true }
dioxus-router = { workspace = true}
dioxus-fullstack = { workspace = true, features = ["router"] }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
tower-http = { version = "0.4.0", features = ["fs"], optional = true }
http = { version = "0.2.9", optional = true }
execute = "0.2.12"
tokio = {workspace = true, features = ["full"], optional = true }
serde = { version = "1.0.159", features = ["derive"] }
[features]
default = ["web"]
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "http"]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
web = ["dioxus-web", "dioxus-router/web"]
@@ -1,122 +1,61 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use dioxus_router::*;
use serde::{Deserialize, Serialize};
use dioxus_router::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
App,
AppProps { route: None },
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use axum::extract::State;
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist/assets folder with the javascript and WASM files created by the CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server
.connect_hot_reload()
// If the path is unknown, render the application
.fallback(
move |uri: http::uri::Uri, State(ssr_state): State<SSRState>| {
let rendered = ssr_state.render(
&ServeConfigBuilder::new(
App,
AppProps {
route: Some(format!("http://{addr}{uri}")),
},
)
.build(),
);
async move { axum::body::Full::from(rendered) }
},
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
launch_router!(@([127, 0, 0, 1], 8080), Route, {
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
});
}
#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)]
struct AppProps {
route: Option<String>,
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/blog/:id")]
Blog { id: i32 },
}
fn App(cx: Scope<AppProps>) -> Element {
cx.render(rsx! {
Router {
initial_url: cx.props.route.clone(),
Route { to: "/blog",
Link {
to: "/",
"Go to counter"
}
table {
tbody {
for _ in 0..100 {
tr {
for _ in 0..100 {
td { "hello world!" }
}
}
#[inline_props]
fn Blog(cx: Scope, id: i32) -> Element {
render! {
Link { target: Route::Home {}, "Go to counter" }
table {
tbody {
for _ in 0..*id {
tr {
for _ in 0..*id {
td { "hello world!" }
}
}
}
},
// Fallback
Route { to: "",
Counter {}
},
}
}
})
}
}
fn Counter(cx: Scope) -> Element {
#[inline_props]
fn Home(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
Link {
to: "/blog",
target: Route::Blog {
id: *count.get()
},
"Go to blog"
}
div{
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
@@ -131,7 +70,7 @@ fn Counter(cx: Scope) -> Element {
}
}
},
"Run a server function"
"Run server function!"
}
"Server said: {text}"
}
@@ -1,2 +1,3 @@
dist
target
target
static
@@ -16,6 +16,6 @@ salvo = { version = "0.37.9", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
default = []
ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"]
web = ["dioxus-web"]
@@ -1,50 +1,19 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case)]
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use salvo::prelude::*;
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let router = Router::new().serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
Server::new(TcpListener::bind("127.0.0.1:8080"))
.serve(router)
.await;
});
}
launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), {
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
});
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
@@ -55,7 +24,6 @@ struct AppProps {
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
@@ -63,12 +31,12 @@ fn app(cx: Scope<AppProps>) -> Element {
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
post_server_data(data).await.unwrap();
}
}
},
@@ -79,10 +47,9 @@ fn app(cx: Scope<AppProps>) -> Element {
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
let cx = server_context();
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
@@ -0,0 +1,3 @@
docs
target
static
@@ -0,0 +1,20 @@
[package]
name = "static-hydrated"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true, features = ["router"] }
dioxus-router = { workspace = true}
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
[features]
default = []
ssr = ["tokio", "dioxus-fullstack/ssr"]
web = ["dioxus-web"]
@@ -0,0 +1,46 @@
[application]
# App (Project) Name
name = "Dioxus"
# Dioxus App Default Platform
# desktop, web, mobile, ssr
default_platform = "web"
# `build` & `serve` dist path
out_dir = "docs"
# resource (public) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "public"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = ["tailwind.css"]
# Javascript code file
script = []
[web.resource.dev]
# serve: [dev-server] only
# CSS style file
style = []
# Javascript code file
script = []
@@ -0,0 +1,86 @@
//! Run with:
//!
//! ```sh
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::{launch, prelude::*};
use dioxus_router::prelude::*;
use serde::{Deserialize, Serialize};
// Generate all routes and output them to the docs path
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
pre_cache_static_routes_with_props(
&ServeConfigBuilder::new_with_router(dioxus_fullstack::router::FullstackRouterConfig::<
Route,
>::default())
.assets_path("docs")
.incremental(IncrementalRendererConfig::default().static_dir("docs"))
.build(),
)
.await
.unwrap();
}
// Hydrate the page
#[cfg(feature = "web")]
fn main() {
dioxus_web::launch_with_props(
dioxus_fullstack::router::RouteWithCfg::<Route>,
dioxus_fullstack::prelude::get_root_props_from_document()
.expect("Failed to get root props from document"),
dioxus_web::Config::default().hydrate(true),
);
}
#[cfg(not(any(feature = "web", feature = "ssr")))]
fn main() {}
#[derive(Clone, Routable, Debug, PartialEq, Serialize, Deserialize)]
enum Route {
#[route("/")]
Home {},
#[route("/blog")]
Blog,
}
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
Link { target: Route::Home {}, "Go to counter" }
table {
tbody {
for _ in 0..100 {
tr {
for _ in 0..100 {
td { "hello world!" }
}
}
}
}
}
}
}
#[inline_props]
fn Home(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
Link {
target: Route::Blog {},
"Go to blog"
}
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
})
}
@@ -1,2 +1,3 @@
dist
target
target
static
@@ -16,6 +16,6 @@ warp = { version = "0.3.3", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
default = []
ssr = ["warp", "tokio", "dioxus-fullstack/warp"]
web = ["dioxus-web"]
@@ -1,47 +1,19 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! dx build --features web
//! cargo run --features ssr
//! ```
#![allow(non_snake_case)]
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let routes = serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
});
}
launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), {
incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
});
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
@@ -52,20 +24,19 @@ struct AppProps {
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 10, "Up high!" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
post_server_data(data).await.unwrap();
}
}
},
@@ -76,10 +47,9 @@ fn app(cx: Scope<AppProps>) -> Element {
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
let cx = server_context();
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
+3 -1
View File
@@ -6,9 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.63"
quote = "1.0.26"
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "15a4e54435eb5a539afb75891292bcccd2cc8e85", features = ["stable"] }
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "6b90e1babd425c9a324181c86e3fd1b942c9b10f" }
syn = { version = "2", features = ["full"] }
convert_case = "0.6.0"
[lib]
proc-macro = true
+145 -10
View File
@@ -1,6 +1,12 @@
use convert_case::{Case, Converter};
use proc_macro::TokenStream;
use quote::ToTokens;
use proc_macro2::Literal;
use quote::{ToTokens, __private::TokenStream as TokenStream2};
use server_fn_macro::*;
use syn::{
parse::{Parse, ParseStream},
Ident, ItemFn, Token,
};
/// Declares that a function is a [server function](dioxus_fullstack). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
@@ -54,18 +60,147 @@ use server_fn_macro::*;
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let context = ServerContext {
ty: syn::parse_quote!(DioxusServerContext),
path: syn::parse_quote!(::dioxus_fullstack::prelude::DioxusServerContext),
// before we pass this off to the server function macro, we apply extractors and middleware
let mut function: syn::ItemFn = match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
// extract all #[middleware] attributes
let mut middlewares: Vec<Middleware> = vec![];
function.attrs.retain(|attr| {
if attr.meta.path().is_ident("middleware") {
if let Ok(middleware) = attr.parse_args() {
middlewares.push(middleware);
false
} else {
true
}
} else {
true
}
});
let ItemFn {
attrs,
vis,
sig,
block,
} = function;
let mapped_body = quote::quote! {
#(#attrs)*
#vis #sig {
#block
}
};
let server_fn_path: syn::Path = syn::parse_quote!(::dioxus_fullstack::prelude::server_fn);
let trait_obj_wrapper: syn::Type =
syn::parse_quote!(::dioxus_fullstack::prelude::ServerFnTraitObj);
let mut args: ServerFnArgs = match syn::parse(args) {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
if args.struct_name.is_none() {
let upper_cammel_case_name = Converter::new()
.from_case(Case::Snake)
.to_case(Case::UpperCamel)
.convert(&sig.ident.to_string());
args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
}
let struct_name = args.struct_name.as_ref().unwrap();
match server_macro_impl(
args.into(),
s.into(),
syn::parse_quote!(::dioxus_fullstack::prelude::ServerFnTraitObj),
Some(context),
Some(syn::parse_quote!(::dioxus_fullstack::prelude::server_fn)),
quote::quote!(#args),
mapped_body,
trait_obj_wrapper,
None,
Some(server_fn_path.clone()),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
Ok(tokens) => quote::quote! {
#tokens
#[cfg(feature = "ssr")]
#server_fn_path::inventory::submit! {
::dioxus_fullstack::prelude::ServerFnMiddleware {
prefix: #struct_name::PREFIX,
url: #struct_name::URL,
middleware: || vec![
#(
std::sync::Arc::new(#middlewares),
),*
]
}
}
}
.to_token_stream()
.into(),
}
}
#[derive(Debug)]
struct Middleware {
expr: syn::Expr,
}
impl ToTokens for Middleware {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let expr = &self.expr;
tokens.extend(quote::quote! {
#expr
});
}
}
impl Parse for Middleware {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg: syn::Expr = input.parse()?;
Ok(Middleware { expr: arg })
}
}
struct ServerFnArgs {
struct_name: Option<Ident>,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Option<Literal>,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl ToTokens for ServerFnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let struct_name = self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
tokens.extend(quote::quote! {
#struct_name
#prefix
#encoding
#fn_path
})
}
}
impl Parse for ServerFnArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse()?;
let _comma3 = input.parse()?;
let fn_path = input.parse()?;
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
}
}
+54 -101
View File
@@ -55,19 +55,17 @@
//! ```
use axum::{
body::{self, Body, BoxBody, Full},
extract::{State, WebSocketUpgrade},
body::{self, Body, BoxBody},
extract::State,
handler::Handler,
http::{Request, Response, StatusCode},
response::IntoResponse,
routing::{get, post},
Router,
};
use dioxus_core::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use server_fn::{Encoding, ServerFunctionRegistry};
use std::sync::Arc;
use tokio::task::spawn_blocking;
use std::sync::RwLock;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
@@ -92,7 +90,7 @@ pub trait DioxusRouterExt<S> {
/// .register_server_fns_with_handler("", |func| {
/// move |req: Request<Body>| async move {
/// let (parts, body) = req.into_parts();
/// let parts: Arc<RequestParts> = Arc::new(parts.into());
/// let parts: Arc<http::request::Parts> = Arc::new(parts.into());
/// let server_context = DioxusServerContext::new(parts.clone());
/// server_fn_handler(server_context, func.clone(), parts, body).await
/// }
@@ -106,7 +104,7 @@ pub trait DioxusRouterExt<S> {
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S>,
@@ -231,7 +229,7 @@ where
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler<T, S, Body>,
@@ -256,11 +254,22 @@ where
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| {
move |req: Request<Body>| async move {
let (parts, body) = req.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts.clone());
server_fn_handler(server_context, func.clone(), parts, body).await
use crate::layer::Service;
move |req: Request<Body>| {
let mut service = crate::server_fn_service(Default::default(), func);
async move {
let (req, body) = req.into_parts();
let req = Request::from_parts(req, body);
let res = service.run(req);
match res.await {
Ok(res) => Ok::<_, std::convert::Infallible>(res.map(|b| b.into())),
Err(_e) => {
let mut res = Response::new(Body::empty());
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Ok(res)
}
}
}
}
})
}
@@ -311,15 +320,13 @@ where
cfg: impl Into<ServeConfig<P>>,
) -> Self {
let cfg = cfg.into();
let ssr_state = SSRState::new(&cfg);
// Add server functions and render index.html
self.serve_static_assets(cfg.assets_path)
.route(
"/",
get(render_handler).with_state((cfg, SSRState::default())),
)
.connect_hot_reload()
.register_server_fns(server_fn_route)
.fallback(get(render_handler).with_state((cfg, ssr_state)))
}
fn connect_hot_reload(self) -> Self {
@@ -330,7 +337,7 @@ where
Router::new()
.route(
"/disconnect",
get(|ws: WebSocketUpgrade| async {
get(|ws: axum::extract::WebSocketUpgrade| async {
ws.on_upgrade(|mut ws| async move {
use axum::extract::ws::Message;
let _ = ws.send(Message::Text("connected".into())).await;
@@ -352,96 +359,42 @@ where
}
}
fn apply_request_parts_to_response<B>(
headers: hyper::header::HeaderMap,
response: &mut axum::response::Response<B>,
) {
let mut_headers = response.headers_mut();
for (key, value) in headers.iter() {
mut_headers.insert(key, value.clone());
}
}
async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
request: Request<Body>,
) -> impl IntoResponse {
let (parts, _) = request.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts);
let mut vdom =
VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context);
let _ = vdom.rebuild();
let url = parts.uri.path_and_query().unwrap().to_string();
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
let server_context = DioxusServerContext::new(parts.clone());
let rendered = ssr_state.render_vdom(&vdom, &cfg);
Full::from(rendered)
}
/// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
parts: Arc<RequestParts>,
body: Body,
) -> impl IntoResponse {
let body = hyper::body::to_bytes(body).await;
let Ok(body) = body else {
return report_err(body.err().unwrap());
};
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
let query_string = parts.uri.query().unwrap_or_default().to_string();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async {
let query = &query_string.into();
let data = match &function.encoding() {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let resp = match function.call(server_context.clone(), data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty response should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(body::boxed(Full::from(data))),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(body::boxed(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(body::boxed(data)),
};
resp.unwrap()
}
Err(e) => report_err(e),
};
resp_tx.send(resp).unwrap();
})
match ssr_state.render(url, &cfg, &server_context).await {
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
let mut response = axum::response::Html::from(html).into_response();
freshness.write(response.headers_mut());
let headers = server_context.response_parts().unwrap().headers.clone();
apply_request_parts_to_response(headers, &mut response);
response
}
});
resp_rx.await.unwrap()
Err(e) => {
log::error!("Failed to render page: {}", e);
report_err(e).into_response()
}
}
}
fn report_err<E: Error>(e: E) -> Response<BoxBody> {
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body::boxed(format!("Error: {}", e)))
@@ -450,7 +403,7 @@ fn report_err<E: Error>(e: E) -> Response<BoxBody> {
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
pub async fn hot_reload_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl IntoResponse {
use axum::extract::ws::Message;
use futures_util::StreamExt;
+142
View File
@@ -16,3 +16,145 @@ pub mod axum_adapter;
pub mod salvo_adapter;
#[cfg(feature = "warp")]
pub mod warp_adapter;
use std::sync::{Arc, RwLock};
use http::StatusCode;
use server_fn::{Encoding, Payload};
use tokio::task::spawn_blocking;
use crate::{
layer::{BoxedService, Service},
prelude::{DioxusServerContext, ProvideServerContext},
};
/// Create a server function handler with the given server context and server function.
pub fn server_fn_service(
context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<()>,
) -> crate::layer::BoxedService {
let prefix = function.prefix().to_string();
let url = function.url().to_string();
if let Some(middleware) = crate::server_fn::MIDDLEWARE.get(&(&prefix, &url)) {
let mut service = BoxedService(Box::new(ServerFnHandler::new(context, function)));
for middleware in middleware {
service = middleware.layer(service);
}
service
} else {
BoxedService(Box::new(ServerFnHandler::new(context, function)))
}
}
#[derive(Clone)]
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub struct ServerFnHandler {
server_context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<()>,
}
impl ServerFnHandler {
/// Create a new server function handler with the given server context and server function.
pub fn new(
server_context: impl Into<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<()>,
) -> Self {
let server_context = server_context.into();
Self {
server_context,
function,
}
}
}
impl Service for ServerFnHandler {
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
> {
let Self {
server_context,
function,
} = self.clone();
Box::pin(async move {
let query = req.uri().query().unwrap_or_default().as_bytes().to_vec();
let (parts, body) = req.into_parts();
let body = hyper::body::to_bytes(body).await?.to_vec();
let headers = &parts.headers;
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(RwLock::new(parts));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
let function = function.clone();
let mut server_context = server_context.clone();
server_context.parts = parts;
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let data = match function.encoding() {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let server_function_future = function.call((), data);
let server_function_future = ProvideServerContext::new(
server_function_future,
server_context.clone(),
);
let resp = server_function_future.await;
resp_tx.send(resp).unwrap();
})
}
});
let result = resp_rx.await.unwrap();
let mut res = http::Response::builder();
// Set the headers from the server context
let parts = server_context.parts.read().unwrap();
*res.headers_mut().expect("empty headers should be valid") = parts.headers.clone();
let serialized = result?;
// if this is Accept: application/json then send a serialized JSON response
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
Ok(match serialized {
Payload::Binary(data) => {
res = res.header("Content-Type", "application/cbor");
res.body(data.into())?
}
Payload::Url(data) => {
res = res.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
);
res.body(data.into())?
}
Payload::Json(data) => {
res = res.header("Content-Type", "application/json");
res.body(data.into())?
}
})
})
}
}
+120 -116
View File
@@ -49,22 +49,31 @@
//! }
//! ```
use dioxus_core::VirtualDom;
use hyper::{http::HeaderValue, StatusCode};
use http_body_util::{BodyExt, Limited};
use hyper::body::Body as HyperBody;
use hyper::StatusCode;
use salvo::{
async_trait, handler,
http::{
cookie::{Cookie, CookieJar},
ParseError,
},
serve_static::{StaticDir, StaticFile},
Depot, FlowCtrl, Handler, Request, Response, Router,
Depot, Error as SalvoError, FlowCtrl, Handler, Request, Response, Router,
};
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use server_fn::{Encoding, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use std::sync::RwLock;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
layer::Service, prelude::*, render::SSRState, serve_config::ServeConfig,
server_fn::DioxusServerFnRegistry, server_fn_service,
};
type HyperRequest = hyper::Request<hyper::Body>;
type HyperResponse = hyper::Response<HyperBody>;
/// A extension trait with utilities for integrating Dioxus with your Salvo router.
pub trait DioxusRouterExt {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
@@ -76,7 +85,7 @@ pub trait DioxusRouterExt {
/// use dioxus_fullstack::prelude::*;
///
/// struct ServerFunctionHandler {
/// server_fn: server_fn::ServerFnTraitObj<DioxusServerContext>,
/// server_fn: server_fn::ServerFnTraitObj<()>,
/// }
///
/// #[handler]
@@ -109,7 +118,7 @@ pub trait DioxusRouterExt {
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
handler: impl Fn(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
handler: impl Fn(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler + 'static;
@@ -203,7 +212,7 @@ impl DioxusRouterExt for Router {
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> H,
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
) -> Self
where
H: Handler + 'static,
@@ -282,7 +291,7 @@ impl DioxusRouterExt for Router {
self.serve_static_assets(cfg.assets_path)
.connect_hot_reload()
.register_server_fns(server_fn_path)
.push(Router::with_path("/").get(SSRHandler { cfg }))
.push(Router::with_path("/<**any_path>").get(SSRHandler { cfg }))
}
fn connect_hot_reload(self) -> Self {
@@ -298,13 +307,71 @@ impl DioxusRouterExt for Router {
}
/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values.
pub fn extract_parts(req: &mut Request) -> RequestParts {
RequestParts {
method: std::mem::take(req.method_mut()),
uri: std::mem::take(req.uri_mut()),
version: req.version(),
headers: std::mem::take(req.headers_mut()),
extensions: std::mem::take(req.extensions_mut()),
pub fn extract_parts(req: &mut Request) -> http::request::Parts {
let mut parts = http::request::Request::new(()).into_parts().0;
parts.method = std::mem::take(req.method_mut());
parts.uri = std::mem::take(req.uri_mut());
parts.version = req.version();
parts.headers = std::mem::take(req.headers_mut());
parts.extensions = std::mem::take(req.extensions_mut());
parts
}
fn apply_request_parts_to_response(
headers: hyper::header::HeaderMap,
response: &mut salvo::prelude::Response,
) {
let mut_headers = response.headers_mut();
for (key, value) in headers.iter() {
mut_headers.insert(key, value.clone());
}
}
#[inline]
async fn convert_request(req: &mut Request) -> Result<HyperRequest, SalvoError> {
let forward_url: hyper::Uri = TryFrom::try_from(req.uri()).map_err(SalvoError::other)?;
let mut build = hyper::Request::builder()
.method(req.method())
.uri(&forward_url);
for (key, value) in req.headers() {
build = build.header(key, value);
}
static SECURE_MAX_SIZE: usize = 64 * 1024;
let body = Limited::new(req.take_body(), SECURE_MAX_SIZE)
.collect()
.await
.map_err(ParseError::other)?
.to_bytes();
build.body(body.into()).map_err(SalvoError::other)
}
#[inline]
async fn convert_response(response: HyperResponse, res: &mut Response) {
let (parts, body) = response.into_parts();
let http::response::Parts {
version,
headers,
status,
..
} = parts;
res.status_code = Some(status);
res.version = version;
res.cookies = CookieJar::new();
for cookie in headers.get_all(http::header::SET_COOKIE).iter() {
if let Some(cookie) = cookie
.to_str()
.ok()
.and_then(|s| Cookie::parse(s.to_string()).ok())
{
res.cookies.add_original(cookie);
}
}
res.headers = headers;
res.version = version;
if let Ok(bytes) = hyper::body::to_bytes(body).await {
res.body = bytes.into()
}
}
@@ -325,34 +392,47 @@ impl<P: Clone + serde::Serialize + Send + Sync + 'static> Handler for SSRHandler
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
renderer.clone()
} else {
let renderer = SSRState::default();
let renderer = SSRState::new(&self.cfg);
depot.inject(renderer.clone());
renderer
};
let parts: Arc<RequestParts> = Arc::new(extract_parts(req));
let route = req.uri().path().to_string();
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(extract_parts(req)));
let server_context = DioxusServerContext::new(parts);
let mut vdom = VirtualDom::new_with_props(self.cfg.app, self.cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
res.write_body(renderer_pool.render_vdom(&vdom, &self.cfg))
.unwrap();
match renderer_pool
.render(route, &self.cfg, &server_context)
.await
{
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
*res.headers_mut() = server_context.take_response_headers();
res.write_body(html).unwrap();
let headers = server_context.response_parts().unwrap().headers.clone();
apply_request_parts_to_response(headers, res);
freshness.write(res.headers_mut());
}
Err(err) => {
log::error!("Error rendering SSR: {}", err);
res.write_body("Error rendering SSR").unwrap();
}
};
}
}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub struct ServerFnHandler {
server_context: DioxusServerContext,
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<()>,
}
impl ServerFnHandler {
/// Create a new server function handler with the given server context and server function.
pub fn new(
server_context: impl Into<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<()>,
) -> Self {
let server_context = server_context.into();
Self {
@@ -365,90 +445,14 @@ impl ServerFnHandler {
#[handler]
impl ServerFnHandler {
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let Self {
server_context,
function,
} = self;
let query = req
.uri()
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let body = hyper::body::to_bytes(req.body_mut().unwrap()).await;
let Ok(body) = body else {
handle_error(body.err().unwrap(), res);
return;
};
let headers = req.headers();
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(extract_parts(req));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
let function = function.clone();
let mut server_context = server_context.clone();
server_context.parts = parts;
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let data = match function.encoding() {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = function.call(server_context, data).await;
resp_tx.send(resp).unwrap();
})
}
});
let result = resp_rx.await.unwrap();
// Set the headers from the server context
*res.headers_mut() = server_context.take_response_headers();
match result {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res.set_status_code(StatusCode::OK);
}
match serialized {
Payload::Binary(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/cbor"));
res.write_body(data).unwrap();
}
Payload::Url(data) => {
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static(
"application/\
x-www-form-urlencoded",
),
);
res.write_body(data).unwrap();
}
Payload::Json(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/json"));
res.write_body(data).unwrap();
}
}
match convert_request(req).await {
Ok(hyper_req) => {
let response =
server_fn_service(self.server_context.clone(), self.function.clone())
.run(hyper_req)
.await
.unwrap();
convert_response(response, res).await;
}
Err(err) => handle_error(err, res),
}
@@ -457,7 +461,7 @@ impl ServerFnHandler {
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
let mut resp_err = Response::new();
resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
resp_err.status_code(StatusCode::INTERNAL_SERVER_ERROR);
resp_err.render(format!("Internal Server Error: {}", error));
*res = resp_err;
}
@@ -494,8 +498,8 @@ impl HotReloadHandler {
_depot: &mut Depot,
res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
use salvo::ws::Message;
use salvo::ws::WebSocketUpgrade;
use salvo::websocket::Message;
use salvo::websocket::WebSocketUpgrade;
let state = crate::hot_reload::spawn_hot_reload().await;
@@ -542,10 +546,10 @@ impl HotReloadHandler {
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[handler]
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
use salvo::ws::WebSocketUpgrade;
use salvo::websocket::WebSocketUpgrade;
WebSocketUpgrade::new()
.upgrade(req, res, |mut ws| async move {
let _ = ws.send(salvo::ws::Message::text("connected")).await;
let _ = ws.send(salvo::websocket::Message::text("connected")).await;
while let Some(msg) = ws.recv().await {
if msg.is_err() {
return;
+79 -142
View File
@@ -46,16 +46,19 @@
//!
//! ```
use crate::layer::Service;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
use dioxus_core::VirtualDom;
use crate::server_fn_service;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use std::sync::RwLock;
use tokio::task::spawn_blocking;
use warp::path::FullPath;
use warp::Rejection;
use warp::{
filters::BoxedFilter,
http::{Response, StatusCode},
@@ -71,19 +74,22 @@ use warp::{
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns_with_handler("", |full_route, func| {
/// let routes = register_server_fns_with_handler(server_fn_route, |full_route, func| {
/// path(full_route)
/// .and(post())
/// .and(header::headers_cloned())
/// .and(body::bytes())
/// .and_then(move |headers: HeaderMap, body| {
/// let func = func.clone();
/// async move {
/// // Add the headers to the server function context
/// server_fn_handler((headers.clone(),), func, headers, body).await
/// }
/// })
/// });
/// .and(warp::post().or(warp::get()).unify())
/// .and(request_parts())
/// .and(warp::body::bytes())
/// .and_then(move |parts, bytes: bytes::Bytes| {
/// let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
/// async move {
/// let req = warp::hyper::Request::from_parts(parts, bytes.into());
/// service.run(req).await.map_err(|err| {
/// log::error!("Server function error: {}", err);
/// warp::reject::reject()
/// })
/// }
/// })
/// })
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
@@ -92,7 +98,7 @@ pub fn register_server_fns_with_handler<H, F, R>(
mut handler: H,
) -> BoxedFilter<(R,)>
where
H: FnMut(String, server_fn::ServerFnTraitObj<DioxusServerContext>) -> F,
H: FnMut(String, server_fn::ServerFnTraitObj<()>) -> F,
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: Send,
R: Reply + 'static,
@@ -131,10 +137,14 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R
.and(warp::post().or(warp::get()).unify())
.and(request_parts())
.and(warp::body::bytes())
.and_then(move |parts, bytes| {
let func = func.clone();
.and_then(move |parts, bytes: bytes::Bytes| {
let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
async move {
server_fn_handler(DioxusServerContext::default(), func, parts, bytes).await
let req = warp::hyper::Request::from_parts(parts, bytes.into());
service.run(req).await.map_err(|err| {
log::error!("Server function error: {}", err);
warp::reject::reject()
})
}
})
})
@@ -168,9 +178,14 @@ pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'sta
let serve_dir = warp::fs::dir(cfg.assets_path);
connect_hot_reload()
// First register the server functions
.or(register_server_fns(server_fn_route))
.or(warp::path::end().and(render_ssr(cfg)))
// Then the index route
.or(path::end().and(render_ssr(cfg.clone())))
// Then the static assets
.or(serve_dir)
// Then all other routes
.or(render_ssr(cfg))
.boxed()
}
@@ -180,32 +195,47 @@ pub fn render_ssr<P: Clone + serde::Serialize + Send + Sync + 'static>(
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::get()
.and(request_parts())
.and(with_ssr_state())
.map(move |parts, renderer: SSRState| {
let parts = Arc::new(parts);
.and(with_ssr_state(&cfg))
.then(move |parts: http::request::Parts, renderer: SSRState| {
let route = parts.uri.path().to_string();
let parts = Arc::new(RwLock::new(parts));
let cfg = cfg.clone();
async move {
let server_context = DioxusServerContext::new(parts);
let server_context = DioxusServerContext::new(parts);
match renderer.render(route, &cfg, &server_context).await {
Ok(rendered) => {
let crate::render::RenderResponse { html, freshness } = rendered;
let mut vdom = VirtualDom::new_with_props(cfg.app, cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
let mut res = Response::builder()
.header("Content-Type", "text/html")
.body(html)
.unwrap();
let html = renderer.render_vdom(&vdom, &cfg);
let headers_mut = res.headers_mut();
let headers = server_context.response_parts().unwrap().headers.clone();
for (key, value) in headers.iter() {
headers_mut.insert(key, value.clone());
}
freshness.write(headers_mut);
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
res.header("Content-Type", "text/html")
.body(Bytes::from(html))
.unwrap()
res
}
Err(err) => {
log::error!("Failed to render ssr: {}", err);
Response::builder()
.status(500)
.body("Failed to render ssr".into())
.unwrap()
}
}
}
})
}
/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
pub fn request_parts(
) -> impl Filter<Extract = (RequestParts,), Error = warp::reject::Rejection> + Clone {
) -> impl Filter<Extract = (http::request::Parts,), Error = warp::reject::Rejection> + Clone {
warp::method()
.and(warp::filters::path::full())
.and(
@@ -221,19 +251,23 @@ pub fn request_parts(
.map_err(|err| {
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
})
.map(|uri| RequestParts {
method,
uri,
headers,
..Default::default()
.map(|uri| {
let mut req = http::Request::builder()
.method(method)
.uri(uri)
.body(())
.unwrap();
req.headers_mut().extend(headers);
req.into_parts().0
})
})
}
fn with_ssr_state() -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone
{
let state = SSRState::default();
warp::any().map(move || state.clone())
fn with_ssr_state<P: Clone + serde::Serialize + Send + Sync + 'static>(
cfg: &ServeConfig<P>,
) -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone {
let renderer = SSRState::new(cfg);
warp::any().map(move || renderer.clone())
}
#[derive(Debug)]
@@ -246,102 +280,6 @@ struct RecieveFailed(String);
impl warp::reject::Reject for RecieveFailed {}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: impl Into<DioxusServerContext>,
function: server_fn::ServerFnTraitObj<DioxusServerContext>,
parts: RequestParts,
body: Bytes,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let mut server_context = server_context.into();
let parts = Arc::new(parts);
server_context.parts = parts.clone();
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let query = parts
.uri
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let data = match function.encoding() {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = match function.call(server_context.clone(), data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.as_ref()
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Bytes::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(Bytes::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(Bytes::from(data)),
};
Box::new(resp.unwrap())
}
Err(e) => report_err(e),
};
if resp_tx.send(resp).is_err() {
eprintln!("Error sending response");
}
})
}
});
resp_rx.await.map_err(|err| {
warp::reject::custom(RecieveFailed(format!("Failed to recieve response {err}")))
})
}
fn report_err<E: Error>(e: E) -> Box<dyn warp::Reply> {
Box::new(
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("Error: {}", e))
.unwrap(),
) as Box<dyn warp::Reply>
}
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
@@ -360,9 +298,8 @@ pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
{
warp::path!("_dioxus" / "hot_reload")
.and(warp::ws())
.map(warp::reply)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND))
}
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
+3 -2
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use dioxus_core::Template;
use dioxus::prelude::Template;
use tokio::sync::{
watch::{channel, Receiver},
RwLock,
@@ -9,7 +9,8 @@ use tokio::sync::{
#[derive(Clone)]
pub struct HotReloadState {
// The cache of all templates that have been modified since the last time we checked
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_core::Template<'static>>>>,
pub(crate) templates:
Arc<RwLock<std::collections::HashSet<dioxus::prelude::Template<'static>>>>,
// The channel to send messages to the hot reload thread
pub(crate) message_receiver: Receiver<Option<Template<'static>>>,
}
+173
View File
@@ -0,0 +1,173 @@
//! Launch helper macros for fullstack apps
#![allow(unused)]
#[macro_export]
/// Launch a server with a router
macro_rules! launch_router {
(@router_config) => {
dioxus_fullstack::router::FullstackRouterConfig::default()
};
(@router_config $router_cfg:expr) => {
$router_cfg
};
(@$address:expr, $route:ty, $(cfg: $router_cfg:expr,)? {$($rule:ident $(: $cfg:expr)?,)*}) => {
dioxus_fullstack::launch!(
@$address,
dioxus_fullstack::router::RouteWithCfg::<$route>,
(dioxus_fullstack::launch_router!(@router_config $($router_cfg)?)),
{
$($rule $(: $cfg)?,)*
}
)
};
}
#[macro_export]
/// Launch a server
macro_rules! launch {
(@web_cfg $server_cfg:ident $wcfg:expr) => {
#[cfg(feature = "web")]
let web_cfg = $wcfg;
};
(@web_cfg $server_cfg:ident) => {
#[cfg(feature = "web")]
let web_cfg = dioxus_web::Config::new();
};
(@serve_cfg $server_cfg:ident $cfg:expr) => {
#[cfg(feature = "ssr")]
let $server_cfg = $cfg;
};
(@hot_reload $server_cfg:ident) => {
#[cfg(feature = "ssr")]
{
dioxus_fullstack::prelude::hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
std::process::Command::new("cargo")
.args(&["run", "--features", "ssr"])
.spawn()
.unwrap()
.wait()
.unwrap();
std::process::Command::new("cargo")
.args(&["run", "--features", "web"])
.spawn()
.unwrap()
.wait()
.unwrap();
true
}));
}
};
(@hot_reload $server_cfg:ident $hot_reload_cfg:expr) => {
#[cfg(feature = "ssr")]
{
dioxus_fullstack::prelude::hot_reload_init!($hot_reload_cfg);
}
};
(@incremental $server_cfg:ident) => {
#[cfg(feature = "ssr")]
let $server_cfg = $server_cfg.incremental(dioxus_fullstack::prelude::IncrementalRendererConfig::default());
};
(@incremental $server_cfg:ident $cfg:expr) => {
#[cfg(feature = "ssr")]
let $server_cfg = $server_cfg.incremental($cfg);
};
(@props_type) => {
Default::default()
};
(@props_type $props:expr) => {
$props
};
(@ $address:expr, $comp:path, $(( $props:expr ),)? {$($rule:ident $(: $cfg:expr)?,)*}) => {
#[cfg(feature = "web")]
{
#[allow(unused)]
let web_cfg = dioxus_web::Config::new();
$(
dioxus_fullstack::prelude::launch!(@$rule server_cfg $($cfg)?);
)*
dioxus_web::launch_with_props(
$comp,
dioxus_fullstack::prelude::get_root_props_from_document().expect("Failed to get root props from document"),
web_cfg.hydrate(true),
);
}
#[cfg(feature = "ssr")]
{
let server_cfg = dioxus_fullstack::prelude::ServeConfigBuilder::new($comp, dioxus_fullstack::prelude::launch!(@props_type $($props)?));
$(
dioxus_fullstack::prelude::launch!(@$rule server_cfg $($cfg)?);
)*
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from($address);
dioxus_fullstack::launch::launch_server(addr, server_cfg.build()).await;
});
}
};
}
/// Launch a server with the given configeration
/// This will use the routing intigration of the currently enabled intigration feature
#[cfg(feature = "ssr")]
pub async fn launch_server<P: Clone + serde::Serialize + Send + Sync + 'static>(
addr: std::net::SocketAddr,
cfg: crate::prelude::ServeConfig<P>,
) {
#[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))]
{
use crate::adapters::axum_adapter::DioxusRouterExt;
use tower::ServiceBuilder;
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application("", cfg)
.layer(
ServiceBuilder::new()
.layer(tower_http::compression::CompressionLayer::new().gzip(true)),
)
.into_make_service(),
)
.await
.unwrap();
}
#[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))]
{
use warp::Filter;
warp::serve(
crate::prelude::serve_dioxus_application("", cfg)
.with(warp::filters::compression::gzip()),
)
.run(addr)
.await;
}
#[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))]
{
use crate::adapters::salvo_adapter::DioxusRouterExt;
use salvo::conn::Listener;
let router = salvo::Router::new().serve_dioxus_application("", cfg).hoop(
salvo::compression::Compression::new()
.enable_gzip(salvo::prelude::CompressionLevel::Default),
);
salvo::Server::new(salvo::conn::tcp::TcpListener::new(addr).bind().await)
.serve(router)
.await;
}
}
+76
View File
@@ -0,0 +1,76 @@
use std::pin::Pin;
use http::{Request, Response};
pub trait Layer: Send + Sync + 'static {
fn layer(&self, inner: BoxedService) -> BoxedService;
}
impl<L> Layer for L
where
L: tower_layer::Layer<BoxedService> + Sync + Send + 'static,
L::Service: Service + Send + 'static,
{
fn layer(&self, inner: BoxedService) -> BoxedService {
BoxedService(Box::new(self.layer(inner)))
}
}
pub trait Service {
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
>;
}
impl<S> Service for S
where
S: tower::Service<http::Request<hyper::body::Body>, Response = Response<hyper::body::Body>>,
S::Future: Send + 'static,
S::Error: Into<server_fn::ServerFnError>,
{
fn run(
&mut self,
req: http::Request<hyper::body::Body>,
) -> Pin<
Box<
dyn std::future::Future<
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
> {
let fut = self.call(req);
Box::pin(async move { fut.await.map_err(|err| err.into()) })
}
}
pub struct BoxedService(pub Box<dyn Service + Send>);
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
type Response = http::Response<hyper::body::Body>;
type Error = server_fn::ServerFnError;
type Future = Pin<
Box<
dyn std::future::Future<
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
> + Send,
>,
>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
Ok(()).into()
}
fn call(&mut self, req: Request<hyper::body::Body>) -> Self::Future {
self.0.run(req)
}
}
+22 -4
View File
@@ -3,17 +3,27 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
pub use adapters::*;
pub use once_cell;
mod props_html;
#[cfg(feature = "router")]
pub mod router;
#[cfg(feature = "ssr")]
mod adapters;
#[cfg(feature = "ssr")]
pub use adapters::*;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
mod hot_reload;
pub mod launch;
#[cfg(feature = "ssr")]
mod layer;
#[cfg(feature = "ssr")]
mod render;
#[cfg(feature = "ssr")]
mod serve_config;
#[cfg(feature = "ssr")]
mod server_context;
mod server_fn;
@@ -27,16 +37,24 @@ pub mod prelude {
pub use crate::adapters::warp_adapter::*;
#[cfg(not(feature = "ssr"))]
pub use crate::props_html::deserialize_props::get_root_props_from_document;
#[cfg(all(feature = "ssr", feature = "router"))]
pub use crate::render::pre_cache_static_routes_with_props;
#[cfg(feature = "ssr")]
pub use crate::render::SSRState;
#[cfg(feature = "ssr")]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
#[cfg(all(feature = "ssr", feature = "axum"))]
pub use crate::server_context::Axum;
#[cfg(feature = "ssr")]
pub use crate::server_context::RequestParts;
pub use crate::server_context::{DioxusServerContext, HasServerContext};
pub use crate::server_context::{
extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext,
};
pub use crate::server_fn::DioxusServerFn;
#[cfg(feature = "ssr")]
pub use crate::server_fn::{ServerFnTraitObj, ServerFunction};
pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction};
pub use crate::{launch, launch_router};
pub use dioxus_server_macro::*;
#[cfg(feature = "ssr")]
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
pub use server_fn::{self, ServerFn as _, ServerFnError};
}
@@ -4,9 +4,8 @@ use base64::engine::general_purpose::STANDARD;
use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_from_string<T: DeserializeOwned>(string: &str) -> Option<T> {
let decompressed = STANDARD.decode(string.as_bytes()).ok()?;
let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap();
pub(crate) fn serde_from_bytes<T: DeserializeOwned>(string: &[u8]) -> Option<T> {
let decompressed = STANDARD.decode(string).ok()?;
postcard::from_bytes(&decompressed).ok()
}
@@ -27,6 +26,6 @@ pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
.get_element_by_id("dioxus-storage")?
.get_attribute("data-serialized")?;
serde_from_string(&attribute)
serde_from_bytes(attribute.as_bytes())
}
}
+3 -3
View File
@@ -23,7 +23,7 @@ fn serialized_and_deserializes() {
for x in 0..10usize {
for y in 0..10 {
let mut as_string = String::new();
let mut as_string: Vec<u8> = Vec::new();
let data = vec![
Data {
a: x as u32,
@@ -39,7 +39,7 @@ fn serialized_and_deserializes() {
];
serialize_props::serde_to_writable(&data, &mut as_string).unwrap();
println!("{}", as_string);
println!("{:?}", as_string);
println!(
"original size: {}",
std::mem::size_of::<Data>() * data.len()
@@ -47,7 +47,7 @@ fn serialized_and_deserializes() {
println!("serialized size: {}", to_allocvec(&data).unwrap().len());
println!("compressed size: {}", as_string.len());
let decoded: Vec<Data> = deserialize_props::serde_from_string(&as_string).unwrap();
let decoded: Vec<Data> = deserialize_props::serde_from_bytes(&as_string).unwrap();
assert_eq!(data, decoded);
}
}
@@ -6,16 +6,10 @@ use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_to_writable<T: Serialize>(
value: &T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
write_to: &mut impl std::io::Write,
) -> std::io::Result<()> {
let serialized = postcard::to_allocvec(value).unwrap();
let compressed = yazi::compress(
&serialized,
yazi::Format::Zlib,
yazi::CompressionLevel::BestSize,
)
.unwrap();
write_to.write_str(&STANDARD.encode(compressed));
write_to.write_all(STANDARD.encode(serialized).as_bytes())?;
Ok(())
}
@@ -23,9 +17,10 @@ pub(crate) fn serde_to_writable<T: Serialize>(
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_in_element<T: Serialize>(
data: T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
serde_to_writable(&data, &mut write_to)?;
write_to.write_str(r#"" />"#)
write_to: &mut impl std::io::Write,
) -> std::io::Result<()> {
write_to
.write_all(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#.as_bytes())?;
serde_to_writable(&data, write_to)?;
write_to.write_all(r#"" />"#.as_bytes())
}
+203 -35
View File
@@ -2,56 +2,152 @@
use std::sync::Arc;
use dioxus_core::VirtualDom;
use dioxus_ssr::Renderer;
use dioxus::prelude::VirtualDom;
use dioxus_ssr::{
incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody},
Renderer,
};
use serde::Serialize;
use crate::prelude::ServeConfig;
use crate::{prelude::*, server_context::with_server_context};
use dioxus::prelude::*;
enum SsrRendererPool {
Renderer(object_pool::Pool<Renderer>),
Incremental(object_pool::Pool<dioxus_ssr::incremental::IncrementalRenderer>),
}
impl SsrRendererPool {
async fn render_to<P: Clone + Serialize + Send + Sync + 'static>(
&self,
cfg: &ServeConfig<P>,
route: String,
component: Component<P>,
props: P,
to: &mut WriteBuffer,
server_context: &DioxusServerContext,
) -> Result<RenderFreshness, dioxus_ssr::incremental::IncrementalRendererError> {
let wrapper = FullstackRenderer { cfg };
match self {
Self::Renderer(pool) => {
let server_context = Box::new(server_context.clone());
let mut vdom = VirtualDom::new_with_props(component, props);
with_server_context(server_context, || {
let _ = vdom.rebuild();
});
let mut renderer = pool.pull(pre_renderer);
// SAFETY: The fullstack renderer will only write UTF-8 to the buffer.
wrapper.render_before_body(&mut **to)?;
renderer.render_to(to, &vdom)?;
wrapper.render_after_body(&mut **to)?;
Ok(RenderFreshness::now(None))
}
Self::Incremental(pool) => {
let mut renderer =
pool.pull(|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap()));
Ok(renderer
.render(
route,
component,
props,
&mut **to,
|vdom| {
let server_context = Box::new(server_context.clone());
with_server_context(server_context, || {
let _ = vdom.rebuild();
});
},
&wrapper,
)
.await?)
}
}
}
}
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
#[derive(Clone)]
pub struct SSRState {
// We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
renderers: Arc<object_pool::Pool<Renderer>>,
renderers: Arc<SsrRendererPool>,
}
impl Default for SSRState {
fn default() -> Self {
impl SSRState {
pub(crate) fn new<P: Clone>(cfg: &ServeConfig<P>) -> Self {
if cfg.incremental.is_some() {
return Self {
renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new(
10,
|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
))),
};
}
Self {
renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new(
10,
pre_renderer,
))),
}
}
/// Render the application to HTML.
pub fn render<'a, P: 'static + Clone + serde::Serialize + Send + Sync>(
&'a self,
route: String,
cfg: &'a ServeConfig<P>,
server_context: &'a DioxusServerContext,
) -> impl std::future::Future<
Output = Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError>,
> + Send
+ 'a {
async move {
let mut html = WriteBuffer { buffer: Vec::new() };
let ServeConfig { app, props, .. } = cfg;
let freshness = self
.renderers
.render_to(cfg, route, *app, props.clone(), &mut html, server_context)
.await?;
Ok(RenderResponse {
html: String::from_utf8(html.buffer).map_err(|err| {
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err))
})?,
freshness,
})
}
}
}
impl SSRState {
/// Render the application to HTML.
pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> String {
let ServeConfig { app, props, .. } = cfg;
struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> {
cfg: &'a ServeConfig<P>,
}
let mut vdom = VirtualDom::new_with_props(*app, props.clone());
impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody
for FullstackRenderer<'a, P>
{
fn render_before_body<R: std::io::Write>(
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
let ServeConfig { index, .. } = &self.cfg;
let _ = vdom.rebuild();
to.write_all(index.pre_main.as_bytes())?;
self.render_vdom(&vdom, cfg)
Ok(())
}
/// Render a VirtualDom to HTML.
pub fn render_vdom<P: 'static + Clone + serde::Serialize>(
fn render_after_body<R: std::io::Write>(
&self,
vdom: &VirtualDom,
cfg: &ServeConfig<P>,
) -> String {
let ServeConfig { index, .. } = cfg;
let mut renderer = self.renderers.pull(pre_renderer);
let mut html = String::new();
html += &index.pre_main;
let _ = renderer.render_to(&mut html, vdom);
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
// serialize the props
let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html);
crate::props_html::serialize_props::encode_in_element(&self.cfg.props, to)?;
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
@@ -82,19 +178,91 @@ impl SSRState {
ws.onclose = reload_upon_connect;
})()"#;
html += r#"<script>"#;
html += disconnect_js;
html += r#"</script>"#;
to.write_all(r#"<script>"#.as_bytes())?;
to.write_all(disconnect_js.as_bytes())?;
to.write_all(r#"</script>"#.as_bytes())?;
}
html += &index.post_main;
let ServeConfig { index, .. } = &self.cfg;
html
to.write_all(index.post_main.as_bytes())?;
Ok(())
}
}
/// A rendered response from the server.
pub struct RenderResponse {
pub(crate) html: String,
pub(crate) freshness: RenderFreshness,
}
impl RenderResponse {
/// Get the rendered HTML.
pub fn html(&self) -> &str {
&self.html
}
/// Get the freshness of the rendered HTML.
pub fn freshness(&self) -> RenderFreshness {
self.freshness
}
}
fn pre_renderer() -> Renderer {
let mut renderer = Renderer::default();
renderer.pre_render = true;
renderer.into()
}
fn incremental_pre_renderer(
cfg: &IncrementalRendererConfig,
) -> dioxus_ssr::incremental::IncrementalRenderer {
let mut renderer = cfg.clone().build();
renderer.renderer_mut().pre_render = true;
renderer
}
#[cfg(all(feature = "ssr", feature = "router"))]
/// Pre-caches all static routes
pub async fn pre_cache_static_routes_with_props<Rt>(
cfg: &crate::prelude::ServeConfig<crate::router::FullstackRouterConfig<Rt>>,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError>
where
Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize,
<Rt as std::str::FromStr>::Err: std::fmt::Display,
{
let wrapper = FullstackRenderer { cfg };
let mut renderer = incremental_pre_renderer(
cfg.incremental
.as_ref()
.expect("incremental renderer config must be set to pre-cache static routes"),
);
dioxus_router::incremental::pre_cache_static_routes::<Rt, _>(&mut renderer, &wrapper).await
}
struct WriteBuffer {
buffer: Vec<u8>,
}
impl std::fmt::Write for WriteBuffer {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.buffer.extend_from_slice(s.as_bytes());
Ok(())
}
}
impl std::ops::Deref for WriteBuffer {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl std::ops::DerefMut for WriteBuffer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buffer
}
}
+108
View File
@@ -0,0 +1,108 @@
//! Fullstack router intigration
#![allow(non_snake_case)]
use dioxus::prelude::*;
/// Used by the launch macro
#[doc(hidden)]
pub fn RouteWithCfg<R>(cx: Scope<FullstackRouterConfig<R>>) -> Element
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
use dioxus_router::prelude::RouterConfig;
#[cfg(feature = "ssr")]
let context = crate::prelude::server_context();
let cfg = *cx.props;
render! {
dioxus_router::prelude::GenericRouter::<R> {
config: move || {
RouterConfig::default()
.failure_external_navigation(cfg.failure_external_navigation)
.history({
#[cfg(feature = "ssr")]
let history = dioxus_router::prelude::MemoryHistory::with_initial_path(
context
.request_parts().unwrap()
.uri
.to_string()
.parse()
.unwrap_or_else(|err| {
log::error!("Failed to parse uri: {}", err);
"/"
.parse()
.unwrap_or_else(|err| {
panic!("Failed to parse uri: {}", err);
})
}),
);
#[cfg(not(feature = "ssr"))]
let history = dioxus_router::prelude::WebHistory::new(
None,
cfg.scroll_restoration,
);
history
})
},
}
}
}
fn default_external_navigation_handler<R>() -> fn(Scope) -> Element
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
dioxus_router::prelude::FailureExternalNavigation::<R>
}
/// The configeration for the router
#[derive(Props, serde::Serialize, serde::Deserialize)]
pub struct FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
#[serde(skip)]
#[serde(default = "default_external_navigation_handler::<R>")]
failure_external_navigation: fn(Scope) -> Element,
scroll_restoration: bool,
#[serde(skip)]
phantom: std::marker::PhantomData<R>,
}
impl<R> Clone for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn clone(&self) -> Self {
Self {
failure_external_navigation: self.failure_external_navigation,
scroll_restoration: self.scroll_restoration,
phantom: std::marker::PhantomData,
}
}
}
impl<R> Copy for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
}
impl<R> Default for FullstackRouterConfig<R>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
failure_external_navigation: dioxus_router::prelude::FailureExternalNavigation::<R>,
scroll_restoration: true,
phantom: std::marker::PhantomData,
}
}
}
+48 -1
View File
@@ -1,10 +1,13 @@
#![allow(non_snake_case)]
//! Configeration for how to serve a Dioxus application
#[cfg(feature = "router")]
use crate::router::*;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use dioxus_core::Component;
use dioxus::prelude::*;
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
#[derive(Clone)]
@@ -14,6 +17,40 @@ pub struct ServeConfigBuilder<P: Clone> {
pub(crate) root_id: Option<&'static str>,
pub(crate) index_path: Option<&'static str>,
pub(crate) assets_path: Option<&'static str>,
pub(crate) incremental:
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
}
/// A template for incremental rendering that does nothing.
#[derive(Default, Clone)]
pub struct EmptyIncrementalRenderTemplate;
impl dioxus_ssr::incremental::WrapBody for EmptyIncrementalRenderTemplate {
fn render_after_body<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
fn render_before_body<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
}
#[cfg(feature = "router")]
impl<R> ServeConfigBuilder<FullstackRouterConfig<R>>
where
R: dioxus_router::prelude::Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
/// Create a new ServeConfigBuilder to serve a router on the server.
pub fn new_with_router(cfg: FullstackRouterConfig<R>) -> Self {
Self::new(RouteWithCfg::<R>, cfg)
}
}
impl<P: Clone> ServeConfigBuilder<P> {
@@ -25,9 +62,16 @@ impl<P: Clone> ServeConfigBuilder<P> {
root_id: None,
index_path: None,
assets_path: None,
incremental: None,
}
}
/// Enable incremental static generation
pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self {
self.incremental = Some(std::sync::Arc::new(cfg));
self
}
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
pub fn index_path(mut self, index_path: &'static str) -> Self {
self.index_path = Some(index_path);
@@ -64,6 +108,7 @@ impl<P: Clone> ServeConfigBuilder<P> {
props: self.props,
index,
assets_path,
incremental: self.incremental,
}
}
}
@@ -106,6 +151,8 @@ pub struct ServeConfig<P: Clone> {
pub(crate) props: P,
pub(crate) index: IndexHtml,
pub(crate) assets_path: &'static str,
pub(crate) incremental:
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
}
impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {
+161 -94
View File
@@ -1,63 +1,33 @@
use dioxus_core::ScopeState;
pub use server_fn_impl::*;
use std::sync::Arc;
use std::sync::RwLock;
/// A trait for an object that contains a server context
pub trait HasServerContext {
/// Get the server context from the state
fn server_context(&self) -> DioxusServerContext;
/// A shortcut for `self.server_context()`
fn sc(&self) -> DioxusServerContext {
self.server_context()
}
}
impl HasServerContext for &ScopeState {
fn server_context(&self) -> DioxusServerContext {
#[cfg(feature = "ssr")]
{
self.consume_context().expect("No server context found")
}
#[cfg(not(feature = "ssr"))]
{
DioxusServerContext {}
}
}
}
/// A shared context for server functions that contains infomation about the request and middleware state.
/// A shared context for server functions that contains information about the request and middleware state.
/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions.
///
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
#[derive(Clone)]
pub struct DioxusServerContext {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc<
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
>,
#[cfg(feature = "ssr")]
headers: std::sync::Arc<std::sync::RwLock<hyper::header::HeaderMap>>,
#[cfg(feature = "ssr")]
pub(crate) parts: std::sync::Arc<RequestParts>,
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
}
#[allow(clippy::derivable_impls)]
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())),
#[cfg(feature = "ssr")]
headers: Default::default(),
#[cfg(feature = "ssr")]
parts: Default::default(),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
}
}
}
#[cfg(feature = "ssr")]
pub use server_fn_impl::*;
#[cfg(feature = "ssr")]
mod server_fn_impl {
use super::*;
use std::sync::LockResult;
@@ -68,11 +38,13 @@ mod server_fn_impl {
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<RequestParts>>) -> Self {
pub fn new(parts: impl Into<Arc<RwLock<http::request::Parts>>>) -> Self {
Self {
parts: parts.into(),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
headers: Default::default(),
response_parts: std::sync::Arc::new(RwLock::new(
http::response::Response::new(()).into_parts().0,
)),
}
}
@@ -92,69 +64,164 @@ mod server_fn_impl {
.map(|_| ())
}
/// Get the headers from the server context
pub fn response_headers(&self) -> RwLockReadGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers()
.expect("Failed to get headers from server context")
/// Get the response parts from the server context
pub fn response_parts(&self) -> LockResult<RwLockReadGuard<'_, http::response::Parts>> {
self.response_parts.read()
}
/// Try to get the headers from the server context
pub fn try_response_headers(
/// Get the response parts from the server context
pub fn response_parts_mut(
&self,
) -> LockResult<RwLockReadGuard<'_, hyper::header::HeaderMap>> {
self.headers.read()
}
/// Get the headers mutably from the server context
pub fn response_headers_mut(&self) -> RwLockWriteGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers_mut()
.expect("Failed to get headers mutably from server context")
}
/// Try to get the headers mut from the server context
pub fn try_response_headers_mut(
&self,
) -> LockResult<RwLockWriteGuard<'_, hyper::header::HeaderMap>> {
self.headers.write()
}
pub(crate) fn take_response_headers(&self) -> hyper::header::HeaderMap {
let mut headers = self.headers.write().unwrap();
std::mem::take(&mut *headers)
) -> LockResult<RwLockWriteGuard<'_, http::response::Parts>> {
self.response_parts.write()
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts(&self) -> &RequestParts {
&self.parts
pub fn request_parts(
&self,
) -> std::sync::LockResult<RwLockReadGuard<'_, http::request::Parts>> {
self.parts.read()
}
}
/// Associated parts of an HTTP Request
#[derive(Debug, Default)]
pub struct RequestParts {
/// The request's method
pub method: http::Method,
/// The request's URI
pub uri: http::Uri,
/// The request's version
pub version: http::Version,
/// The request's headers
pub headers: http::HeaderMap<http::HeaderValue>,
/// The request's extensions
pub extensions: http::Extensions,
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts_mut(
&self,
) -> std::sync::LockResult<RwLockWriteGuard<'_, http::request::Parts>> {
self.parts.write()
}
impl From<http::request::Parts> for RequestParts {
fn from(parts: http::request::Parts) -> Self {
Self {
method: parts.method,
uri: parts.uri,
version: parts.version,
headers: parts.headers,
extensions: parts.extensions,
}
/// Extract some part from the request
pub async fn extract<R: std::error::Error, T: FromServerContext<Rejection = R>>(
&self,
) -> Result<T, R> {
T::from_request(self).await
}
}
}
std::thread_local! {
static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() ));
}
/// Get information about the current server request.
///
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
pub fn server_context() -> DioxusServerContext {
SERVER_CONTEXT.with(|ctx| *ctx.borrow().clone())
}
/// Extract some part from the current server request.
///
/// This function will only provide the current server context if it is called from a server function or on the server rendering a request.
pub async fn extract<E: FromServerContext<I>, I>() -> Result<E, E::Rejection> {
E::from_request(&server_context()).await
}
pub(crate) fn with_server_context<O>(
context: Box<DioxusServerContext>,
f: impl FnOnce() -> O,
) -> (O, Box<DioxusServerContext>) {
// before polling the future, we need to set the context
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(context));
// poll the future, which may call server_context()
let result = f();
// after polling the future, we need to restore the context
(result, SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)))
}
/// A future that provides the server context to the inner future
#[pin_project::pin_project]
pub struct ProvideServerContext<F: std::future::Future> {
context: Option<Box<DioxusServerContext>>,
#[pin]
f: F,
}
impl<F: std::future::Future> ProvideServerContext<F> {
/// Create a new future that provides the server context to the inner future
pub fn new(f: F, context: DioxusServerContext) -> Self {
Self {
context: Some(Box::new(context)),
f,
}
}
}
impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
type Output = F::Output;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.project();
let context = this.context.take().unwrap();
let (result, context) = with_server_context(context, || this.f.poll(cx));
*this.context = Some(context);
result
}
}
/// A trait for extracting types from the server context
#[async_trait::async_trait(?Send)]
pub trait FromServerContext<I = ()>: Sized {
/// The error type returned when extraction fails. This type must implement `std::error::Error`.
type Rejection: std::error::Error;
/// Extract this type from the server context.
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection>;
}
/// A type was not found in the server context
pub struct NotFoundInServerContext<T: 'static>(std::marker::PhantomData<T>);
impl<T: 'static> std::fmt::Debug for NotFoundInServerContext<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let type_name = std::any::type_name::<T>();
write!(f, "`{type_name}` not found in server context")
}
}
impl<T: 'static> std::fmt::Display for NotFoundInServerContext<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let type_name = std::any::type_name::<T>();
write!(f, "`{type_name}` not found in server context")
}
}
impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub(crate) T);
#[async_trait::async_trait(?Send)]
impl<T: Send + Sync + Clone + 'static> FromServerContext for FromContext<T> {
type Rejection = NotFoundInServerContext<T>;
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(Self(req.clone().get::<T>().ok_or_else(|| {
NotFoundInServerContext::<T>(std::marker::PhantomData::<T>)
})?))
}
}
#[cfg(feature = "axum")]
/// An adapter for axum extractors for the server context
pub struct Axum;
#[cfg(feature = "axum")]
#[async_trait::async_trait(?Send)]
impl<
I: axum::extract::FromRequestParts<(), Rejection = R>,
R: axum::response::IntoResponse + std::error::Error,
> FromServerContext<Axum> for I
{
type Rejection = R;
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
Ok(I::from_request_parts(&mut *req.request_parts_mut().unwrap(), &()).await?)
}
}
+42 -13
View File
@@ -1,13 +1,11 @@
use crate::server_context::DioxusServerContext;
#[cfg(any(feature = "ssr", doc))]
#[derive(Clone)]
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<DioxusServerContext>);
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>);
#[cfg(any(feature = "ssr", doc))]
impl std::ops::Deref for ServerFnTraitObj {
type Target = server_fn::ServerFnTraitObj<DioxusServerContext>;
type Target = server_fn::ServerFnTraitObj<()>;
fn deref(&self) -> &Self::Target {
&self.0
@@ -33,9 +31,7 @@ impl ServerFnTraitObj {
}
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
pub const fn from_generic_server_fn(
server_fn: server_fn::ServerFnTraitObj<DioxusServerContext>,
) -> Self {
pub const fn from_generic_server_fn(server_fn: server_fn::ServerFnTraitObj<()>) -> Self {
Self(server_fn)
}
}
@@ -43,9 +39,42 @@ impl ServerFnTraitObj {
#[cfg(any(feature = "ssr", doc))]
server_fn::inventory::collect!(ServerFnTraitObj);
#[cfg(any(feature = "ssr", doc))]
/// Middleware for a server function
pub struct ServerFnMiddleware {
/// The prefix of the server function.
pub prefix: &'static str,
/// The url of the server function.
pub url: &'static str,
/// The middleware layers.
pub middleware: fn() -> Vec<std::sync::Arc<dyn crate::layer::Layer>>,
}
#[cfg(any(feature = "ssr", doc))]
pub(crate) static MIDDLEWARE: once_cell::sync::Lazy<
std::collections::HashMap<
(&'static str, &'static str),
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
>,
> = once_cell::sync::Lazy::new(|| {
let mut map: std::collections::HashMap<
(&'static str, &'static str),
Vec<std::sync::Arc<dyn crate::layer::Layer>>,
> = std::collections::HashMap::new();
for middleware in server_fn::inventory::iter::<ServerFnMiddleware> {
map.entry((middleware.prefix, middleware.url))
.or_default()
.extend((middleware.middleware)().iter().cloned());
}
map
});
#[cfg(any(feature = "ssr", doc))]
server_fn::inventory::collect!(ServerFnMiddleware);
#[cfg(any(feature = "ssr", doc))]
/// A server function that can be called on serializable arguments and returns a serializable result.
pub type ServerFunction = server_fn::SerializedFnTraitObj<DioxusServerContext>;
pub type ServerFunction = server_fn::SerializedFnTraitObj<()>;
#[cfg(any(feature = "ssr", doc))]
#[allow(clippy::type_complexity)]
@@ -64,7 +93,7 @@ static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
pub struct DioxusServerFnRegistry;
#[cfg(feature = "ssr")]
impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRegistry {
impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
type Error = ServerRegistrationFnError;
fn register_explicit(
@@ -105,7 +134,7 @@ impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRe
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<DioxusServerContext>> {
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
@@ -113,7 +142,7 @@ impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRe
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<DioxusServerContext>> {
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
Self::get(url)
}
@@ -155,7 +184,7 @@ pub enum ServerRegistrationFnError {
/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself.
pub trait DioxusServerFn: server_fn::ServerFn<DioxusServerContext> {
pub trait DioxusServerFn: server_fn::ServerFn<()> {
/// Registers the server function, allowing the client to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
@@ -163,4 +192,4 @@ pub trait DioxusServerFn: server_fn::ServerFn<DioxusServerContext> {
}
}
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<DioxusServerContext> {}
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<()> {}
+1
View File
@@ -1098,6 +1098,7 @@ builder_constructors! {
autofocus: Bool DEFAULT,
capture: String DEFAULT,
checked: Bool DEFAULT,
directory: Bool "webkitdirectory",
disabled: Bool DEFAULT,
form: Id DEFAULT,
formaction: Uri DEFAULT,
+1
View File
@@ -25,6 +25,7 @@ const bool_attrs = {
reversed: true,
selected: true,
truespeed: true,
webkitdirectory: true,
};
export function setAttributeInner(node, field, value, ns) {
@@ -163,6 +163,7 @@ mod js {
reversed: true,
selected: true,
truespeed: true,
webkitdirectory: true,
};
function truthy(val) {
return val === "true" || val === true;
@@ -11,13 +11,14 @@ pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket {
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
fn transform_rx(message: Result<Message, axum::Error>) -> Result<Vec<u8>, LiveViewError> {
message
.map_err(|_| LiveViewError::SendingFailed)?
.into_text()
.map(|s| s.into_bytes())
.map_err(|_| LiveViewError::SendingFailed)
}
async fn transform_tx(message: String) -> Result<Message, axum::Error> {
Ok(Message::Text(message))
async fn transform_tx(message: Vec<u8>) -> Result<Message, axum::Error> {
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
}
@@ -12,14 +12,12 @@ pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<String, LiveViewError> {
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<Vec<u8>, LiveViewError> {
let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
Ok(msg)
Ok(as_bytes.into())
}
async fn transform_tx(message: String) -> Result<Message, salvo::Error> {
Ok(Message::text(message))
async fn transform_tx(message: Vec<u8>) -> Result<Message, salvo::Error> {
Ok(Message::text(String::from_utf8_lossy(&message).to_string()))
}
@@ -11,18 +11,15 @@ pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
fn transform_rx(message: Result<Message, warp::Error>) -> Result<Vec<u8>, LiveViewError> {
// destructure the message into the buffer we got from warp
let msg = message
.map_err(|_| LiveViewError::SendingFailed)?
.into_bytes();
// transform it back into a string, saving us the allocation
let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
Ok(msg)
}
async fn transform_tx(message: String) -> Result<Message, warp::Error> {
Ok(Message::text(message))
async fn transform_tx(message: Vec<u8>) -> Result<Message, warp::Error> {
Ok(Message::text(String::from_utf8_lossy(&message).to_string()))
}
+15 -11
View File
@@ -87,16 +87,16 @@ impl LiveViewPool {
/// }
/// ```
pub trait LiveViewSocket:
SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
SinkExt<Vec<u8>, Error = LiveViewError>
+ StreamExt<Item = Result<Vec<u8>, LiveViewError>>
+ Send
+ 'static
{
}
impl<S> LiveViewSocket for S where
S: SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
S: SinkExt<Vec<u8>, Error = LiveViewError>
+ StreamExt<Item = Result<Vec<u8>, LiveViewError>>
+ Send
+ 'static
{
@@ -126,7 +126,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
pin_mut!(ws);
// send the initial render to the client
ws.send(edits).await?;
ws.send(edits.into_bytes()).await?;
// Create the a proxy for query engine
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -156,11 +156,11 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
evt = ws.next() => {
match evt.as_ref().map(|o| o.as_deref()) {
// respond with a pong every ping to keep the websocket alive
Some(Ok("__ping__")) => {
ws.send("__pong__".to_string()).await?;
Some(Ok(b"__ping__")) => {
ws.send(b"__pong__".to_vec()).await?;
}
Some(Ok(evt)) => {
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
if let Ok(message) = serde_json::from_str::<IpcMessage>(&String::from_utf8_lossy(evt)) {
match message {
IpcMessage::Event(evt) => {
// Intercept the mounted event and insert a custom element type
@@ -196,7 +196,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
// handle any new queries
Some(query) = query_rx.recv() => {
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?;
}
Some(msg) = hot_reload_wait => {
@@ -218,8 +218,12 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
.await?;
ws.send(
serde_json::to_string(&ClientUpdate::Edits(edits))
.unwrap()
.into_bytes(),
)
.await?;
}
}
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "dioxus-router-macro"
version = "0.3.0"
authors = ["Evan Almloff"]
edition = "2021"
description = "Macro for Dioxus Router"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "router"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0.11", features = ["extra-traits", "full"] }
quote = "1.0"
proc-macro2 = "1.0.56"
slab = "0.4"
[features]
default = []
File renamed without changes.
+62
View File
@@ -0,0 +1,62 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::Path;
use crate::nest::{Nest, NestId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutId(pub usize);
#[derive(Debug)]
pub struct Layout {
pub comp: Path,
pub props_name: Path,
pub active_nests: Vec<NestId>,
}
impl Layout {
pub fn routable_match(&self, nests: &[Nest]) -> TokenStream {
let props_name = &self.props_name;
let comp_name = &self.comp;
let name_str = self.comp.segments.last().unwrap().ident.to_string();
let dynamic_segments = self
.active_nests
.iter()
.flat_map(|id| nests[id.0].dynamic_segments());
quote! {
let comp = #props_name { #(#dynamic_segments,)* };
let dynamic = cx.component(#comp_name, comp, #name_str);
render! {
dynamic
}
}
}
}
impl Layout {
pub fn parse(input: syn::parse::ParseStream, active_nests: Vec<NestId>) -> syn::Result<Self> {
// Then parse the component name
let _ = input.parse::<syn::Token![,]>();
let comp: Path = input.parse()?;
// Then parse the props name
let _ = input.parse::<syn::Token![,]>();
let props_name = input.parse::<Path>().unwrap_or_else(|_| {
let last = format_ident!("{}Props", comp.segments.last().unwrap().ident.to_string());
let mut segments = comp.segments.clone();
segments.pop();
segments.push(last.into());
Path {
leading_colon: None,
segments,
}
});
Ok(Self {
comp,
props_name,
active_nests,
})
}
}
+714
View File
@@ -0,0 +1,714 @@
extern crate proc_macro;
use layout::Layout;
use nest::{Nest, NestId};
use proc_macro::TokenStream;
use quote::{__private::Span, format_ident, quote, ToTokens};
use redirect::Redirect;
use route::{Route, RouteType};
use segment::RouteSegment;
use syn::{parse::ParseStream, parse_macro_input, Ident, Token, Type};
use proc_macro2::TokenStream as TokenStream2;
use crate::{layout::LayoutId, route_tree::RouteTree};
mod layout;
mod nest;
mod query;
mod redirect;
mod route;
mod route_tree;
mod segment;
/// Derives the Routable trait for an enum of routes
///
/// Each variant must:
/// 1. Be struct-like with {}'s
/// 2. Contain all of the dynamic parameters of the current and nested routes
/// 3. Have a `#[route("route")]` attribute
///
/// Route Segments:
/// 1. Static Segments: "/static"
/// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants)
/// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants)
/// 4. Query Segments: "/?:query" (where query has a type that is FromQuery in all child Variants)
///
/// Routes are matched:
/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route")
/// 2. By the order they are defined in the enum
///
/// All features:
/// ```rust, skip
/// #[rustfmt::skip]
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// // Define routes with the route macro. If the name of the component is not the same as the variant, you can specify it as the second parameter and the props type as the third
/// #[route("/", IndexComponent, ComponentProps)]
/// Index {},
/// // Nests with parameters have types taken from child routes
/// // Everything inside the nest has the added parameter `user_id: usize`
/// #[nest("/user/:user_id")]
/// // All children of layouts will be rendered inside the Outlet in the layout component
/// // Creates a Layout UserFrame that has the parameter `user_id: usize`
/// #[layout(UserFrame)]
/// // If there is a component with the name Route1 and props with the name Route1Props, you do not need to pass in the component and type
/// #[route("/:dynamic?:query")]
/// Route1 {
/// // The type is taken from the first instance of the dynamic parameter
/// user_id: usize,
/// dynamic: usize,
/// query: String,
/// extra: String,
/// },
/// #[route("/hello_world")]
/// // You can opt out of the layout by using the `!` prefix
/// #[layout(!UserFrame)]
/// Route2 { user_id: usize },
/// // End layouts with #[end_layout]
/// #[end_layout]
/// // End nests with #[end_nest]
/// #[end_nest]
/// // Redirects take a path and a function that takes the parameters from the path and returns a new route
/// #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
/// #[route("/:dynamic")]
/// Route3 { dynamic: String },
/// #[child]
/// NestedRoute(NestedRoute),
/// }
/// ```
///
/// # `#[route("path", component, props)]`
///
/// The `#[route]` attribute is used to define a route. It takes up to 3 parameters:
/// - `path`: The path to the enum variant (relative to the parent nest)
/// - (optional) `component`: The component to render when the route is matched. If not specified, the name of the variant is used
/// - (optional) `props`: The props type for the component. If not specified, the name of the variant with `Props` appended is used
///
/// Routes are the most basic attribute. They allow you to define a route and the component to render when the route is matched. The component must take all dynamic parameters of the route and all parent nests.
/// The next variant will be tied to the component. If you link to that variant, the component will be rendered.
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// // Define routes that renders the IndexComponent that takes the IndexProps
/// // The Index component will be rendered when the route is matched (e.g. when the user navigates to /)
/// #[route("/", Index, IndexProps)]
/// Index {},
/// }
/// ```
///
/// # `#[redirect("path", function)]`
///
/// The `#[redirect]` attribute is used to define a redirect. It takes 2 parameters:
/// - `path`: The path to the enum variant (relative to the parent nest)
/// - `function`: A function that takes the parameters from the path and returns a new route
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// // Redirects the /:id route to the Index route
/// #[redirect("/:id", |_: usize| Route::Index {})]
/// #[route("/", Index, IndexProps)]
/// Index {},
/// }
/// ```
///
/// Redirects allow you to redirect a route to another route. The function must take all dynamic parameters of the route and all parent nests.
///
/// # `#[nest("path")]`
///
/// The `#[nest]` attribute is used to define a nest. It takes 1 parameter:
/// - `path`: The path to the nest (relative to the parent nest)
///
/// Nests effect all nests, routes and redirects defined until the next `#[end_nest]` attribute. All children of nests are relative to the nest route and must include all dynamic parameters of the nest.
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// // Nests all child routes in the /blog route
/// #[nest("/blog")]
/// // This is at /blog/:id
/// #[redirect("/:id", |_: usize| Route::Index {})]
/// // This is at /blog
/// #[route("/", Index, IndexProps)]
/// Index {},
/// }
/// ```
///
/// # `#[end_nest]`
///
/// The `#[end_nest]` attribute is used to end a nest. It takes no parameters.
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// #[nest("/blog")]
/// // This is at /blog/:id
/// #[redirect("/:id", |_: usize| Route::Index {})]
/// // This is at /blog
/// #[route("/", Index, IndexProps)]
/// Index {},
/// // Ends the nest
/// #[end_nest]
/// // This is at /
/// #[route("/")]
/// Home {},
/// }
/// ```
///
/// # `#[layout(component)]`
///
/// The `#[layout]` attribute is used to define a layout. It takes 2 parameters:
/// - `component`: The component to render when the route is matched. If not specified, the name of the variant is used
/// - (optional) `props`: The props type for the component. If not specified, the name of the variant with `Props` appended is used
///
/// The layout component allows you to wrap all children of the layout in a component. The child routes are rendered in the Outlet of the layout component. The layout component must take all dynamic parameters of the nests it is nested in.
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// #[layout(BlogFrame)]
/// #[redirect("/:id", |_: usize| Route::Index {})]
/// // Index will be rendered in the Outlet of the BlogFrame component
/// #[route("/", Index, IndexProps)]
/// Index {},
/// }
/// ```
///
/// # `#[end_layout]`
///
/// The `#[end_layout]` attribute is used to end a layout. It takes no parameters.
///
/// ```rust, skip
/// #[derive(Clone, Debug, PartialEq, Routable)]
/// enum Route {
/// #[layout(BlogFrame)]
/// #[redirect("/:id", |_: usize| Route::Index {})]
/// // Index will be rendered in the Outlet of the BlogFrame component
/// #[route("/", Index, IndexProps)]
/// Index {},
/// // Ends the layout
/// #[end_layout]
/// // This will be rendered standalone
/// #[route("/")]
/// Home {},
/// }
/// ```
#[proc_macro_derive(
Routable,
attributes(route, nest, end_nest, layout, end_layout, redirect, child)
)]
pub fn routable(input: TokenStream) -> TokenStream {
let routes_enum = parse_macro_input!(input as syn::ItemEnum);
let route_enum = match RouteEnum::parse(routes_enum) {
Ok(route_enum) => route_enum,
Err(err) => return err.to_compile_error().into(),
};
let error_type = route_enum.error_type();
let parse_impl = route_enum.parse_impl();
let display_impl = route_enum.impl_display();
let routable_impl = route_enum.routable_impl();
let name = &route_enum.name;
let vis = &route_enum.vis;
quote! {
#vis fn Outlet(cx: dioxus::prelude::Scope) -> dioxus::prelude::Element {
dioxus_router::prelude::GenericOutlet::<#name>(cx)
}
#vis fn Router(cx: dioxus::prelude::Scope<dioxus_router::prelude::GenericRouterProps<#name>>) -> dioxus::prelude::Element {
dioxus_router::prelude::GenericRouter(cx)
}
#vis fn Link<'a>(cx: dioxus::prelude::Scope<'a, dioxus_router::prelude::GenericLinkProps<'a, #name>>) -> dioxus::prelude::Element<'a> {
dioxus_router::prelude::GenericLink(cx)
}
#vis fn GoBackButton<'a>(cx: dioxus::prelude::Scope<'a, dioxus_router::prelude::GenericHistoryButtonProps<'a>>) -> dioxus::prelude::Element<'a> {
dioxus_router::prelude::GenericGoBackButton::<#name>(cx)
}
#vis fn GoForwardButton<'a>(cx: dioxus::prelude::Scope<'a, dioxus_router::prelude::GenericHistoryButtonProps<'a>>) -> dioxus::prelude::Element<'a> {
dioxus_router::prelude::GenericGoForwardButton::<#name>(cx)
}
#vis fn use_route(cx: &dioxus::prelude::ScopeState) -> Option<#name> {
dioxus_router::prelude::use_generic_route(cx)
}
#vis fn use_navigator(cx: &dioxus::prelude::ScopeState) -> &dioxus_router::prelude::GenericNavigator<#name> {
dioxus_router::prelude::use_generic_navigator(cx)
}
#error_type
#display_impl
#routable_impl
#parse_impl
}
.into()
}
struct RouteEnum {
vis: syn::Visibility,
name: Ident,
redirects: Vec<Redirect>,
routes: Vec<Route>,
nests: Vec<Nest>,
layouts: Vec<Layout>,
site_map: Vec<SiteMapSegment>,
}
impl RouteEnum {
fn parse(data: syn::ItemEnum) -> syn::Result<Self> {
let name = &data.ident;
let vis = &data.vis;
let mut site_map = Vec::new();
let mut site_map_stack: Vec<Vec<SiteMapSegment>> = Vec::new();
let mut routes = Vec::new();
let mut redirects = Vec::new();
let mut layouts: Vec<Layout> = Vec::new();
let mut layout_stack = Vec::new();
let mut nests = Vec::new();
let mut nest_stack = Vec::new();
for variant in &data.variants {
let mut excluded = Vec::new();
// Apply the any nesting attributes in order
for attr in &variant.attrs {
if attr.path.is_ident("nest") {
let mut children_routes = Vec::new();
{
// add all of the variants of the enum to the children_routes until we hit an end_nest
let mut level = 0;
'o: for variant in &data.variants {
children_routes.push(variant.fields.clone());
for attr in &variant.attrs {
if attr.path.is_ident("nest") {
level += 1;
} else if attr.path.is_ident("end_nest") {
level -= 1;
if level < 0 {
break 'o;
}
}
}
}
}
let nest_index = nests.len();
let parser = |input: ParseStream| {
Nest::parse(
input,
children_routes
.iter()
.filter_map(|f: &syn::Fields| match f {
syn::Fields::Named(fields) => Some(fields.clone()),
_ => None,
})
.collect(),
nest_index,
)
};
let nest = attr.parse_args_with(parser)?;
// add the current segment to the site map stack
let segments: Vec<_> = nest
.segments
.iter()
.map(|seg| {
let segment_type = seg.into();
SiteMapSegment {
segment_type,
children: Vec::new(),
}
})
.collect();
if !segments.is_empty() {
site_map_stack.push(segments);
}
nests.push(nest);
nest_stack.push(NestId(nest_index));
} else if attr.path.is_ident("end_nest") {
nest_stack.pop();
// pop the current nest segment off the stack and add it to the parent or the site map
if let Some(segment) = site_map_stack.pop() {
let children = site_map_stack
.last_mut()
.map(|seg| &mut seg.last_mut().unwrap().children)
.unwrap_or(&mut site_map);
// Turn the list of segments in the segments stack into a tree
let mut iter = segment.into_iter().rev();
let mut current = iter.next().unwrap();
for mut segment in iter {
segment.children.push(current);
current = segment;
}
children.push(current);
}
} else if attr.path.is_ident("layout") {
let parser = |input: ParseStream| {
let bang: Option<Token![!]> = input.parse().ok();
let exclude = bang.is_some();
Ok((exclude, Layout::parse(input, nest_stack.clone())?))
};
let (exclude, layout): (bool, Layout) = attr.parse_args_with(parser)?;
if exclude {
let Some(layout_index) = layouts.iter().position(|l| l.comp == layout.comp)
else {
return Err(syn::Error::new(
Span::call_site(),
"Attempted to exclude a layout that does not exist",
));
};
excluded.push(LayoutId(layout_index));
} else {
let layout_index = layouts.len();
layouts.push(layout);
layout_stack.push(LayoutId(layout_index));
}
} else if attr.path.is_ident("end_layout") {
layout_stack.pop();
} else if attr.path.is_ident("redirect") {
let parser = |input: ParseStream| {
Redirect::parse(input, nest_stack.clone(), redirects.len())
};
let redirect = attr.parse_args_with(parser)?;
redirects.push(redirect);
}
}
let active_nests = nest_stack.clone();
let mut active_layouts = layout_stack.clone();
active_layouts.retain(|&id| !excluded.contains(&id));
let route = Route::parse(active_nests, active_layouts, variant.clone())?;
// add the route to the site map
let mut segment = SiteMapSegment::new(&route.segments);
if let RouteType::Child(child) = &route.ty {
let new_segment = SiteMapSegment {
segment_type: SegmentType::Child(child.ty.clone()),
children: Vec::new(),
};
match &mut segment {
Some(segment) => {
fn set_last_child_to(
segment: &mut SiteMapSegment,
new_segment: SiteMapSegment,
) {
if let Some(last) = segment.children.last_mut() {
set_last_child_to(last, new_segment);
} else {
segment.children = vec![new_segment];
}
}
set_last_child_to(segment, new_segment);
}
None => {
segment = Some(new_segment);
}
}
}
if let Some(segment) = segment {
let parent = site_map_stack.last_mut();
let children = match parent {
Some(parent) => &mut parent.last_mut().unwrap().children,
None => &mut site_map,
};
children.push(segment);
}
routes.push(route);
}
// pop any remaining site map segments
while let Some(segment) = site_map_stack.pop() {
let children = site_map_stack
.last_mut()
.map(|seg| &mut seg.last_mut().unwrap().children)
.unwrap_or(&mut site_map);
// Turn the list of segments in the segments stack into a tree
let mut iter = segment.into_iter().rev();
let mut current = iter.next().unwrap();
for mut segment in iter {
segment.children.push(current);
current = segment;
}
children.push(current);
}
let myself = Self {
vis: vis.clone(),
name: name.clone(),
routes,
redirects,
nests,
layouts,
site_map,
};
Ok(myself)
}
fn impl_display(&self) -> TokenStream2 {
let mut display_match = Vec::new();
for route in &self.routes {
display_match.push(route.display_match(&self.nests));
}
let name = &self.name;
quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[allow(unused)]
match self {
#(#display_match)*
}
Ok(())
}
}
}
}
fn parse_impl(&self) -> TokenStream2 {
let tree = RouteTree::new(&self.routes, &self.nests, &self.redirects);
let name = &self.name;
let error_name = format_ident!("{}MatchError", self.name);
let tokens = tree.roots.iter().map(|&id| {
let route = tree.get(id).unwrap();
route.to_tokens(&self.nests, &tree, self.name.clone(), error_name.clone())
});
quote! {
impl<'a> core::convert::TryFrom<&'a str> for #name {
type Error = <Self as std::str::FromStr>::Err;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl std::str::FromStr for #name {
type Err = dioxus_router::routable::RouteParseError<#error_name>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let route = s;
let (route, _hash) = route.split_once('#').unwrap_or((route, ""));
let (route, query) = route.split_once('?').unwrap_or((route, ""));
let mut segments = route.split('/');
// skip the first empty segment
if s.starts_with('/') {
segments.next();
}
let mut errors = Vec::new();
#(#tokens)*
Err(dioxus_router::routable::RouteParseError {
attempted_routes: errors,
})
}
}
}
}
fn error_name(&self) -> Ident {
Ident::new(&(self.name.to_string() + "MatchError"), Span::call_site())
}
fn error_type(&self) -> TokenStream2 {
let match_error_name = self.error_name();
let mut type_defs = Vec::new();
let mut error_variants = Vec::new();
let mut display_match = Vec::new();
for route in &self.routes {
let route_name = &route.route_name;
let error_name = route.error_ident();
let route_str = &route.route;
error_variants.push(quote! { #route_name(#error_name) });
display_match.push(quote! { Self::#route_name(err) => write!(f, "Route '{}' ('{}') did not match:\n{}", stringify!(#route_name), #route_str, err)? });
type_defs.push(route.error_type());
}
for nest in &self.nests {
let error_variant = nest.error_variant();
let error_name = nest.error_ident();
let route_str = &nest.route;
error_variants.push(quote! { #error_variant(#error_name) });
display_match.push(quote! { Self::#error_variant(err) => write!(f, "Nest '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
type_defs.push(nest.error_type());
}
for redirect in &self.redirects {
let error_variant = redirect.error_variant();
let error_name = redirect.error_ident();
let route_str = &redirect.route;
error_variants.push(quote! { #error_variant(#error_name) });
display_match.push(quote! { Self::#error_variant(err) => write!(f, "Redirect '{}' ('{}') did not match:\n{}", stringify!(#error_name), #route_str, err)? });
type_defs.push(redirect.error_type());
}
quote! {
#(#type_defs)*
#[allow(non_camel_case_types)]
#[derive(Debug, PartialEq)]
pub enum #match_error_name {
#(#error_variants),*
}
impl std::fmt::Display for #match_error_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#(#display_match),*
}
Ok(())
}
}
}
}
fn routable_impl(&self) -> TokenStream2 {
let name = &self.name;
let site_map = &self.site_map;
let mut matches = Vec::new();
// Collect all routes matches
for route in &self.routes {
matches.push(route.routable_match(&self.layouts, &self.nests));
}
quote! {
impl dioxus_router::routable::Routable for #name where Self: Clone {
const SITE_MAP: &'static [dioxus_router::routable::SiteMapSegment] = &[
#(#site_map,)*
];
fn render<'a>(&self, cx: &'a dioxus::prelude::ScopeState, level: usize) -> dioxus::prelude::Element<'a> {
let myself = self.clone();
match (level, myself) {
#(#matches)*
_ => None
}
}
}
}
}
}
struct SiteMapSegment {
pub segment_type: SegmentType,
pub children: Vec<SiteMapSegment>,
}
impl SiteMapSegment {
fn new(segments: &[RouteSegment]) -> Option<Self> {
let mut current = None;
// walk backwards through the new segments, adding children as we go
for segment in segments.iter().rev() {
let segment_type = segment.into();
let mut segment = SiteMapSegment {
segment_type,
children: Vec::new(),
};
// if we have a current segment, add it as a child
if let Some(current) = current.take() {
segment.children.push(current)
}
current = Some(segment);
}
current
}
}
impl ToTokens for SiteMapSegment {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let segment_type = &self.segment_type;
let children = if let SegmentType::Child(ty) = &self.segment_type {
quote! { #ty::SITE_MAP }
} else {
let children = self
.children
.iter()
.map(|child| child.to_token_stream())
.collect::<Vec<_>>();
quote! {
&[
#(#children,)*
]
}
};
tokens.extend(quote! {
dioxus_router::routable::SiteMapSegment {
segment_type: #segment_type,
children: #children,
}
});
}
}
enum SegmentType {
Static(String),
Dynamic(String),
CatchAll(String),
Child(Type),
}
impl ToTokens for SegmentType {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
SegmentType::Static(s) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::Static(#s) })
}
SegmentType::Dynamic(s) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::Dynamic(#s) })
}
SegmentType::CatchAll(s) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::CatchAll(#s) })
}
SegmentType::Child(_) => {
tokens.extend(quote! { dioxus_router::routable::SegmentType::Child })
}
}
}
}
impl<'a> From<&'a RouteSegment> for SegmentType {
fn from(value: &'a RouteSegment) -> Self {
match value {
segment::RouteSegment::Static(s) => SegmentType::Static(s.to_string()),
segment::RouteSegment::Dynamic(s, _) => SegmentType::Dynamic(s.to_string()),
segment::RouteSegment::CatchAll(s, _) => SegmentType::CatchAll(s.to_string()),
}
}
}
+87
View File
@@ -0,0 +1,87 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, LitStr};
use crate::segment::{create_error_type, parse_route_segments, RouteSegment};
#[derive(Debug, Clone, Copy)]
pub struct NestId(pub usize);
#[derive(Debug, Clone)]
pub struct Nest {
pub route: String,
pub segments: Vec<RouteSegment>,
index: usize,
}
impl Nest {
pub fn parse(
input: syn::parse::ParseStream,
children_routes: Vec<syn::FieldsNamed>,
index: usize,
) -> syn::Result<Self> {
// Parse the route
let route: LitStr = input.parse()?;
let route_segments = parse_route_segments(
route.span(),
children_routes
.iter()
.flat_map(|f| f.named.iter())
.map(|f| (f.ident.as_ref().unwrap(), &f.ty)),
&route.value(),
)?
.0;
for seg in &route_segments {
if let RouteSegment::CatchAll(name, _) = seg {
return Err(syn::Error::new_spanned(
name,
format!(
"Catch-all segments are not allowed in nested routes: {}",
route.value()
),
));
}
}
Ok(Self {
route: route.value(),
segments: route_segments,
index,
})
}
}
impl Nest {
pub fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream> + '_ {
self.dynamic_segments_names().map(|i| quote! {#i})
}
pub fn dynamic_segments_names(&self) -> impl Iterator<Item = Ident> + '_ {
self.segments.iter().filter_map(|seg| seg.name())
}
pub fn write(&self) -> TokenStream {
let write_segments = self.segments.iter().map(|s| s.write_segment());
quote! {
{
#(#write_segments)*
}
}
}
pub fn error_ident(&self) -> Ident {
format_ident!("Nest{}ParseError", self.index)
}
pub fn error_variant(&self) -> Ident {
format_ident!("Nest{}", self.index)
}
pub fn error_type(&self) -> TokenStream {
let error_name = self.error_ident();
create_error_type(error_name, &self.segments, None)
}
}
+27
View File
@@ -0,0 +1,27 @@
use quote::quote;
use syn::{Ident, Type};
use proc_macro2::TokenStream as TokenStream2;
#[derive(Debug)]
pub struct QuerySegment {
pub ident: Ident,
pub ty: Type,
}
impl QuerySegment {
pub fn parse(&self) -> TokenStream2 {
let ident = &self.ident;
let ty = &self.ty;
quote! {
let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query);
}
}
pub fn write(&self) -> TokenStream2 {
let ident = &self.ident;
quote! {
write!(f, "?{}", #ident)?;
}
}
}
+91
View File
@@ -0,0 +1,91 @@
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::LitStr;
use crate::{
nest::NestId,
query::QuerySegment,
segment::{create_error_type, parse_route_segments, RouteSegment},
};
#[derive(Debug)]
pub(crate) struct Redirect {
pub route: LitStr,
pub nests: Vec<NestId>,
pub segments: Vec<RouteSegment>,
pub query: Option<QuerySegment>,
pub function: syn::ExprClosure,
pub index: usize,
}
impl Redirect {
pub fn error_ident(&self) -> Ident {
format_ident!("Redirect{}ParseError", self.index)
}
pub fn error_variant(&self) -> Ident {
format_ident!("Redirect{}", self.index)
}
pub fn error_type(&self) -> TokenStream {
let error_name = self.error_ident();
create_error_type(error_name, &self.segments, None)
}
pub fn parse_query(&self) -> TokenStream {
match &self.query {
Some(query) => query.parse(),
None => quote! {},
}
}
pub fn parse(
input: syn::parse::ParseStream,
active_nests: Vec<NestId>,
index: usize,
) -> syn::Result<Self> {
let path = input.parse::<syn::LitStr>()?;
let _ = input.parse::<syn::Token![,]>();
let function = input.parse::<syn::ExprClosure>()?;
let mut closure_arguments = Vec::new();
for arg in function.inputs.iter() {
match arg {
syn::Pat::Type(pat) => match &*pat.pat {
syn::Pat::Ident(ident) => {
closure_arguments.push((ident.ident.clone(), (*pat.ty).clone()));
}
_ => {
return Err(syn::Error::new_spanned(
arg,
"Expected closure argument to be a typed pattern",
))
}
},
_ => {
return Err(syn::Error::new_spanned(
arg,
"Expected closure argument to be a typed pattern",
))
}
}
}
let (segments, query) = parse_route_segments(
path.span(),
closure_arguments.iter().map(|(name, ty)| (name, ty)),
&path.value(),
)?;
Ok(Redirect {
route: path,
nests: active_nests,
segments,
query,
function,
index,
})
}
}
+368
View File
@@ -0,0 +1,368 @@
use quote::{format_ident, quote};
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_quote;
use syn::Field;
use syn::Path;
use syn::Type;
use syn::{Ident, LitStr};
use proc_macro2::TokenStream as TokenStream2;
use crate::layout::Layout;
use crate::layout::LayoutId;
use crate::nest::Nest;
use crate::nest::NestId;
use crate::query::QuerySegment;
use crate::segment::create_error_type;
use crate::segment::parse_route_segments;
use crate::segment::RouteSegment;
struct RouteArgs {
route: LitStr,
comp_name: Option<Path>,
props_name: Option<Path>,
}
impl Parse for RouteArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let route = input.parse::<LitStr>()?;
Ok(RouteArgs {
route,
comp_name: {
let _ = input.parse::<syn::Token![,]>();
input.parse().ok()
},
props_name: {
let _ = input.parse::<syn::Token![,]>();
input.parse().ok()
},
})
}
}
struct ChildArgs {
route: LitStr,
}
impl Parse for ChildArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let route = input.parse::<LitStr>()?;
Ok(ChildArgs { route })
}
}
#[derive(Debug)]
pub(crate) struct Route {
pub route_name: Ident,
pub ty: RouteType,
pub route: String,
pub segments: Vec<RouteSegment>,
pub query: Option<QuerySegment>,
pub nests: Vec<NestId>,
pub layouts: Vec<LayoutId>,
fields: Vec<(Ident, Type)>,
}
impl Route {
pub fn parse(
nests: Vec<NestId>,
layouts: Vec<LayoutId>,
variant: syn::Variant,
) -> syn::Result<Self> {
let route_attr = variant
.attrs
.iter()
.find(|attr| attr.path.is_ident("route"));
let route;
let ty;
let route_name = variant.ident.clone();
match route_attr {
Some(attr) => {
let args = attr.parse_args::<RouteArgs>()?;
let comp_name = args.comp_name.unwrap_or_else(|| parse_quote!(#route_name));
let props_name = args.props_name.unwrap_or_else(|| {
let last = format_ident!(
"{}Props",
comp_name.segments.last().unwrap().ident.to_string()
);
let mut segments = comp_name.segments.clone();
segments.pop();
segments.push(last.into());
Path {
leading_colon: None,
segments,
}
});
ty = RouteType::Leaf {
component: comp_name,
props: props_name,
};
route = args.route.value();
}
None => {
if let Some(route_attr) = variant
.attrs
.iter()
.find(|attr| attr.path.is_ident("child"))
{
let args = route_attr.parse_args::<ChildArgs>()?;
route = args.route.value();
match &variant.fields {
syn::Fields::Named(fields) => {
// find either a field with #[child] or a field named "child"
let child_field = fields.named.iter().find(|f| {
f.attrs
.iter()
.any(|attr| attr.path.is_ident("child"))
|| *f.ident.as_ref().unwrap() == "child"
});
match child_field{
Some(child) => {
ty = RouteType::Child(child.clone());
}
None => {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants with a #[child(..)] attribute must have a field named \"child\" or a field with a #[child] attribute",
));
}
}
}
_ => {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants with a #[child(..)] attribute must have named fields",
))
}
}
} else {
return Err(syn::Error::new_spanned(
variant.clone(),
"Routable variants must either have a #[route(..)] attribute or a #[child(..)] attribute",
));
}
}
};
let fields = match &variant.fields {
syn::Fields::Named(fields) => fields
.named
.iter()
.filter_map(|f| {
if let RouteType::Child(child) = &ty {
if f.ident == child.ident {
return None;
}
}
Some((f.ident.clone().unwrap(), f.ty.clone()))
})
.collect(),
_ => Vec::new(),
};
let (route_segments, query) = {
parse_route_segments(
variant.ident.span(),
fields.iter().map(|f| (&f.0, &f.1)),
&route,
)?
};
Ok(Self {
ty,
route_name,
segments: route_segments,
route,
query,
nests,
layouts,
fields,
})
}
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
let name = &self.route_name;
let dynamic_segments = self.dynamic_segments();
let write_query = self.query.as_ref().map(|q| q.write());
match &self.ty {
RouteType::Child(field) => {
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
let write_segments = self.segments.iter().map(|s| s.write_segment());
let child = field.ident.as_ref().unwrap();
quote! {
Self::#name { #(#dynamic_segments,)* #child } => {
use std::fmt::Display;
use std::fmt::Write;
let mut route = String::new();
{
let f = &mut route;
#(#write_nests)*
#(#write_segments)*
}
if route.ends_with('/') {
route.pop();
}
f.write_str(&route)?;
#child.fmt(f)?;
}
}
}
RouteType::Leaf { .. } => {
let write_nests = self.nests.iter().map(|id| nests[id.0].write());
let write_segments = self.segments.iter().map(|s| s.write_segment());
quote! {
Self::#name { #(#dynamic_segments,)* } => {
#(#write_nests)*
#(#write_segments)*
#write_query
}
}
}
}
}
pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
let name = &self.route_name;
let name_str = name.to_string();
let mut tokens = TokenStream2::new();
// First match all layouts
for (idx, layout_id) in self.layouts.iter().copied().enumerate() {
let render_layout = layouts[layout_id.0].routable_match(nests);
let dynamic_segments = self.dynamic_segments();
let mut field_name = None;
if let RouteType::Child(field) = &self.ty {
field_name = field.ident.as_ref();
}
let field_name = field_name.map(|f| quote!(#f,));
// This is a layout
tokens.extend(quote! {
#[allow(unused)]
(#idx, Self::#name { #(#dynamic_segments,)* #field_name .. }) => {
#render_layout
}
});
}
// Then match the route
let last_index = self.layouts.len();
tokens.extend(match &self.ty {
RouteType::Child(field) => {
let field_name = field.ident.as_ref().unwrap();
quote! {
#[allow(unused)]
(#last_index.., Self::#name { #field_name, .. }) => {
#field_name.render(cx, level - #last_index)
}
}
}
RouteType::Leaf { component, props } => {
let dynamic_segments = self.dynamic_segments();
let dynamic_segments_from_route = self.dynamic_segments();
quote! {
#[allow(unused)]
(#last_index, Self::#name { #(#dynamic_segments,)* }) => {
let comp = #props { #(#dynamic_segments_from_route,)* };
let dynamic = cx.component(#component, comp, #name_str);
render! {
dynamic
}
}
}
}
});
tokens
}
fn dynamic_segments(&self) -> impl Iterator<Item = TokenStream2> + '_ {
self.fields.iter().map(|(name, _)| {
quote! {#name}
})
}
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
let segments = self.fields.iter().map(|(name, _)| {
let mut from_route = false;
for id in &self.nests {
let nest = &nests[id.0];
if nest.dynamic_segments_names().any(|i| &i == name) {
from_route = true
}
}
for segment in &self.segments {
if let RouteSegment::Dynamic(other, ..) = segment {
if other == name {
from_route = true
}
}
}
if let Some(query) = &self.query {
if &query.ident == name {
from_route = true
}
}
if from_route {
quote! {#name}
} else {
quote! {#name: Default::default()}
}
});
match &self.ty {
RouteType::Child(field) => {
let name = &self.route_name;
let child_name = field.ident.as_ref().unwrap();
quote! {
#enum_name::#name {
#child_name,
#(#segments,)*
}
}
}
RouteType::Leaf { .. } => {
let name = &self.route_name;
quote! {
#enum_name::#name {
#(#segments,)*
}
}
}
}
}
pub fn error_ident(&self) -> Ident {
format_ident!("{}ParseError", self.route_name)
}
pub fn error_type(&self) -> TokenStream2 {
let error_name = self.error_ident();
let child_type = match &self.ty {
RouteType::Child(field) => Some(&field.ty),
RouteType::Leaf { .. } => None,
};
create_error_type(error_name, &self.segments, child_type)
}
pub fn parse_query(&self) -> TokenStream2 {
match &self.query {
Some(query) => query.parse(),
None => quote! {},
}
}
}
#[derive(Debug)]
pub(crate) enum RouteType {
Child(Field),
Leaf { component: Path, props: Path },
}
+599
View File
@@ -0,0 +1,599 @@
use proc_macro2::TokenStream;
use quote::quote;
use slab::Slab;
use syn::Ident;
use crate::{
nest::{Nest, NestId},
redirect::Redirect,
route::{Route, RouteType},
segment::{static_segment_idx, RouteSegment},
};
#[derive(Debug, Clone, Default)]
pub(crate) struct RouteTree<'a> {
pub roots: Vec<usize>,
entries: Slab<RouteTreeSegmentData<'a>>,
}
impl<'a> RouteTree<'a> {
pub fn get(&self, index: usize) -> Option<&RouteTreeSegmentData<'a>> {
self.entries.get(index)
}
pub fn get_mut(&mut self, element: usize) -> Option<&mut RouteTreeSegmentData<'a>> {
self.entries.get_mut(element)
}
fn sort_children(&mut self) {
let mut old_roots = self.roots.clone();
self.sort_ids(&mut old_roots);
self.roots = old_roots;
for id in self.roots.clone() {
self.sort_children_of_id(id);
}
}
fn sort_ids(&self, ids: &mut [usize]) {
ids.sort_by_key(|&seg| {
let seg = self.get(seg).unwrap();
match seg {
RouteTreeSegmentData::Static { .. } => 0,
RouteTreeSegmentData::Nest { .. } => 1,
RouteTreeSegmentData::Route(route) => {
// Routes that end in a catch all segment should be checked last
match route.segments.last() {
Some(RouteSegment::CatchAll(..)) => 2,
_ => 1,
}
}
RouteTreeSegmentData::Redirect(redirect) => {
// Routes that end in a catch all segment should be checked last
match redirect.segments.last() {
Some(RouteSegment::CatchAll(..)) => 2,
_ => 1,
}
}
}
});
}
fn sort_children_of_id(&mut self, id: usize) {
// Sort segments so that all static routes are checked before dynamic routes
let mut children = self.children(id);
self.sort_ids(&mut children);
if let Some(old) = self.try_children_mut(id) {
old.clone_from(&children)
}
for id in children {
self.sort_children_of_id(id);
}
}
fn children(&self, element: usize) -> Vec<usize> {
let element = self.entries.get(element).unwrap();
match element {
RouteTreeSegmentData::Static { children, .. } => children.clone(),
RouteTreeSegmentData::Nest { children, .. } => children.clone(),
_ => Vec::new(),
}
}
fn try_children_mut(&mut self, element: usize) -> Option<&mut Vec<usize>> {
let element = self.entries.get_mut(element).unwrap();
match element {
RouteTreeSegmentData::Static { children, .. } => Some(children),
RouteTreeSegmentData::Nest { children, .. } => Some(children),
_ => None,
}
}
fn children_mut(&mut self, element: usize) -> &mut Vec<usize> {
self.try_children_mut(element)
.expect("Cannot get children of non static or nest segment")
}
pub(crate) fn new(routes: &'a [Route], nests: &'a [Nest], redirects: &'a [Redirect]) -> Self {
let routes = routes
.iter()
.map(|route| PathIter::new_route(route, nests))
.chain(
redirects
.iter()
.map(|redirect| PathIter::new_redirect(redirect, nests)),
)
.collect::<Vec<_>>();
let mut myself = Self::default();
myself.roots = myself.construct(routes);
myself.sort_children();
myself
}
pub fn construct(&mut self, routes: Vec<PathIter<'a>>) -> Vec<usize> {
let mut segments = Vec::new();
// Add all routes to the tree
for mut route in routes {
let mut current_route: Option<usize> = None;
// First add all nests
while let Some(nest) = route.next_nest() {
let segments_iter = nest.segments.iter();
// Add all static segments of the nest
'o: for (index, segment) in segments_iter.enumerate() {
match segment {
RouteSegment::Static(segment) => {
// Check if the segment already exists
{
// Either look for the segment in the current route or in the static segments
let segments = current_route
.map(|id| self.children(id))
.unwrap_or_else(|| segments.clone());
for &seg_id in segments.iter() {
let seg = self.get(seg_id).unwrap();
if let RouteTreeSegmentData::Static { segment: s, .. } = seg {
if *s == segment {
// If it does, just update the current route
current_route = Some(seg_id);
continue 'o;
}
}
}
}
let static_segment = RouteTreeSegmentData::Static {
segment,
children: Vec::new(),
error_variant: StaticErrorVariant {
varient_parse_error: nest.error_ident(),
enum_varient: nest.error_variant(),
},
index,
};
// If it doesn't, add the segment to the current route
let static_segment = self.entries.insert(static_segment);
let current_children = current_route
.map(|id| self.children_mut(id))
.unwrap_or_else(|| &mut segments);
current_children.push(static_segment);
// Update the current route
current_route = Some(static_segment);
}
// If there is a dynamic segment, stop adding static segments
RouteSegment::Dynamic(..) => break,
RouteSegment::CatchAll(..) => {
todo!("Catch all segments are not allowed in nests")
}
}
}
// Add the nest to the current route
let nest = RouteTreeSegmentData::Nest {
nest,
children: Vec::new(),
};
let nest = self.entries.insert(nest);
let segments = match current_route.and_then(|id| self.get_mut(id)) {
Some(RouteTreeSegmentData::Static { children, .. }) => children,
Some(RouteTreeSegmentData::Nest { children, .. }) => children,
Some(r) => {
unreachable!("{current_route:?}\n{r:?} is not a static or nest segment",)
}
None => &mut segments,
};
segments.push(nest);
// Update the current route
current_route = Some(nest);
}
match route.next_static_segment() {
// If there is a static segment, check if it already exists in the tree
Some((i, segment)) => {
let current_children = current_route
.map(|id| self.children(id))
.unwrap_or_else(|| segments.clone());
let found = current_children.iter().find_map(|&id| {
let seg = self.get(id).unwrap();
match seg {
RouteTreeSegmentData::Static { segment: s, .. } => {
(s == &segment).then_some(id)
}
_ => None,
}
});
match found {
Some(id) => {
// If it exists, add the route to the children of the segment
let new_children = self.construct(vec![route]);
self.children_mut(id).extend(new_children.into_iter());
}
None => {
// If it doesn't exist, add the route as a new segment
let data = RouteTreeSegmentData::Static {
segment,
error_variant: route.error_variant(),
children: self.construct(vec![route]),
index: i,
};
let id = self.entries.insert(data);
let current_children_mut = current_route
.map(|id| self.children_mut(id))
.unwrap_or_else(|| &mut segments);
current_children_mut.push(id);
}
}
}
// If there is no static segment, add the route to the current_route
None => {
let id = self.entries.insert(route.final_segment);
let current_children_mut = current_route
.map(|id| self.children_mut(id))
.unwrap_or_else(|| &mut segments);
current_children_mut.push(id);
}
}
}
segments
}
}
#[derive(Debug, Clone)]
pub struct StaticErrorVariant {
varient_parse_error: Ident,
enum_varient: Ident,
}
// First deduplicate the routes by the static part of the route
#[derive(Debug, Clone)]
pub(crate) enum RouteTreeSegmentData<'a> {
Static {
segment: &'a str,
error_variant: StaticErrorVariant,
index: usize,
children: Vec<usize>,
},
Nest {
nest: &'a Nest,
children: Vec<usize>,
},
Route(&'a Route),
Redirect(&'a Redirect),
}
impl<'a> RouteTreeSegmentData<'a> {
pub fn to_tokens(
&self,
nests: &[Nest],
tree: &RouteTree,
enum_name: syn::Ident,
error_enum_name: syn::Ident,
) -> TokenStream {
match self {
RouteTreeSegmentData::Static {
segment,
children,
index,
error_variant:
StaticErrorVariant {
varient_parse_error,
enum_varient,
},
} => {
let children = children.iter().map(|child| {
let child = tree.get(*child).unwrap();
child.to_tokens(nests, tree, enum_name.clone(), error_enum_name.clone())
});
if segment.is_empty() {
return quote! {
{
#(#children)*
}
};
}
let error_ident = static_segment_idx(*index);
quote! {
{
let mut segments = segments.clone();
if let Some(segment) = segments.next() {
if #segment == segment {
#(#children)*
}
else {
errors.push(#error_enum_name::#enum_varient(#varient_parse_error::#error_ident(segment.to_string())))
}
}
}
}
}
RouteTreeSegmentData::Route(route) => {
// At this point, we have matched all static segments, so we can just check if the remaining segments match the route
let varient_parse_error = route.error_ident();
let enum_varient = &route.route_name;
let route_segments = route
.segments
.iter()
.enumerate()
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
let construct_variant = route.construct(nests, enum_name);
let parse_query = route.parse_query();
let insure_not_trailing = match route.ty {
RouteType::Leaf { .. } => route
.segments
.last()
.map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
.unwrap_or(true),
RouteType::Child(_) => false,
};
let print_route_segment = print_route_segment(
route_segments.peekable(),
return_constructed(
insure_not_trailing,
construct_variant,
&error_enum_name,
enum_varient,
&varient_parse_error,
parse_query,
),
&error_enum_name,
enum_varient,
&varient_parse_error,
);
match &route.ty {
RouteType::Child(child) => {
let ty = &child.ty;
let child_name = &child.ident;
quote! {
let mut trailing = String::new();
for seg in segments.clone() {
trailing += seg;
trailing += "/";
}
trailing.pop();
match #ty::from_str(&trailing).map_err(|err| #error_enum_name::#enum_varient(#varient_parse_error::ChildRoute(err))) {
Ok(#child_name) => {
#print_route_segment
}
Err(err) => {
errors.push(err);
}
}
}
}
RouteType::Leaf { .. } => print_route_segment,
}
}
Self::Nest { nest, children } => {
// At this point, we have matched all static segments, so we can just check if the remaining segments match the route
let varient_parse_error: Ident = nest.error_ident();
let enum_varient = nest.error_variant();
let route_segments = nest
.segments
.iter()
.enumerate()
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
let parse_children = children
.iter()
.map(|child| {
let child = tree.get(*child).unwrap();
child.to_tokens(nests, tree, enum_name.clone(), error_enum_name.clone())
})
.collect();
print_route_segment(
route_segments.peekable(),
parse_children,
&error_enum_name,
&enum_varient,
&varient_parse_error,
)
}
Self::Redirect(redirect) => {
// At this point, we have matched all static segments, so we can just check if the remaining segments match the route
let varient_parse_error = redirect.error_ident();
let enum_varient = &redirect.error_variant();
let route_segments = redirect
.segments
.iter()
.enumerate()
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
let parse_query = redirect.parse_query();
let insure_not_trailing = redirect
.segments
.last()
.map(|seg| !matches!(seg, RouteSegment::CatchAll(_, _)))
.unwrap_or(true);
let redirect_function = &redirect.function;
let args = redirect_function.inputs.iter().map(|pat| match pat {
syn::Pat::Type(ident) => {
let name = &ident.pat;
quote! {#name}
}
_ => panic!("Expected closure argument to be a typed pattern"),
});
let return_redirect = quote! {
(#redirect_function)(#(#args,)*)
};
print_route_segment(
route_segments.peekable(),
return_constructed(
insure_not_trailing,
return_redirect,
&error_enum_name,
enum_varient,
&varient_parse_error,
parse_query,
),
&error_enum_name,
enum_varient,
&varient_parse_error,
)
}
}
}
}
fn print_route_segment<'a, I: Iterator<Item = (usize, &'a RouteSegment)>>(
mut s: std::iter::Peekable<I>,
sucess_tokens: TokenStream,
error_enum_name: &Ident,
enum_varient: &Ident,
varient_parse_error: &Ident,
) -> TokenStream {
if let Some((i, route)) = s.next() {
let children = print_route_segment(
s,
sucess_tokens,
error_enum_name,
enum_varient,
varient_parse_error,
);
route.try_parse(
i,
error_enum_name,
enum_varient,
varient_parse_error,
children,
)
} else {
quote! {
#sucess_tokens
}
}
}
fn return_constructed(
insure_not_trailing: bool,
construct_variant: TokenStream,
error_enum_name: &Ident,
enum_varient: &Ident,
varient_parse_error: &Ident,
parse_query: TokenStream,
) -> TokenStream {
if insure_not_trailing {
quote! {
let remaining_segments = segments.clone();
let mut segments_clone = segments.clone();
let next_segment = segments_clone.next();
let segment_after_next = segments_clone.next();
match (next_segment, segment_after_next) {
// This is the last segment, return the parsed route
(None, _) | (Some(""), None) => {
#parse_query
return Ok(#construct_variant);
}
_ => {
let mut trailing = String::new();
for seg in remaining_segments {
trailing += seg;
trailing += "/";
}
trailing.pop();
errors.push(#error_enum_name::#enum_varient(#varient_parse_error::ExtraSegments(trailing)))
}
}
}
} else {
quote! {
#parse_query
return Ok(#construct_variant);
}
}
}
pub struct PathIter<'a> {
final_segment: RouteTreeSegmentData<'a>,
active_nests: &'a [NestId],
all_nests: &'a [Nest],
segments: &'a [RouteSegment],
error_ident: Ident,
error_variant: Ident,
nest_index: usize,
static_segment_index: usize,
}
impl<'a> PathIter<'a> {
fn new_route(route: &'a Route, nests: &'a [Nest]) -> Self {
Self {
final_segment: RouteTreeSegmentData::Route(route),
active_nests: &*route.nests,
segments: &*route.segments,
error_ident: route.error_ident(),
error_variant: route.route_name.clone(),
all_nests: nests,
nest_index: 0,
static_segment_index: 0,
}
}
fn new_redirect(redirect: &'a Redirect, nests: &'a [Nest]) -> Self {
Self {
final_segment: RouteTreeSegmentData::Redirect(redirect),
active_nests: &*redirect.nests,
segments: &*redirect.segments,
error_ident: redirect.error_ident(),
error_variant: redirect.error_variant(),
all_nests: nests,
nest_index: 0,
static_segment_index: 0,
}
}
fn next_nest(&mut self) -> Option<&'a Nest> {
let idx = self.nest_index;
let nest_index = self.active_nests.get(idx)?;
let nest = &self.all_nests[nest_index.0];
self.nest_index += 1;
Some(nest)
}
fn next_static_segment(&mut self) -> Option<(usize, &'a str)> {
let idx = self.static_segment_index;
let segment = self.segments.get(idx)?;
match segment {
RouteSegment::Static(segment) => {
self.static_segment_index += 1;
Some((idx, segment))
}
_ => None,
}
}
fn error_variant(&self) -> StaticErrorVariant {
StaticErrorVariant {
varient_parse_error: self.error_ident.clone(),
enum_varient: self.error_variant.clone(),
}
}
}
+295
View File
@@ -0,0 +1,295 @@
use quote::{format_ident, quote};
use syn::{Ident, Type};
use proc_macro2::{Span, TokenStream as TokenStream2};
use crate::query::QuerySegment;
#[derive(Debug, Clone)]
pub enum RouteSegment {
Static(String),
Dynamic(Ident, Type),
CatchAll(Ident, Type),
}
impl RouteSegment {
pub fn name(&self) -> Option<Ident> {
match self {
Self::Static(_) => None,
Self::Dynamic(ident, _) => Some(ident.clone()),
Self::CatchAll(ident, _) => Some(ident.clone()),
}
}
pub fn write_segment(&self) -> TokenStream2 {
match self {
Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; },
Self::CatchAll(ident, _) => quote! { #ident.display_route_segements(f)?; },
}
}
pub fn error_name(&self, idx: usize) -> Ident {
match self {
Self::Static(_) => static_segment_idx(idx),
Self::Dynamic(ident, _) => format_ident!("{}ParseError", ident),
Self::CatchAll(ident, _) => format_ident!("{}ParseError", ident),
}
}
pub fn missing_error_name(&self) -> Option<Ident> {
match self {
Self::Dynamic(ident, _) => Some(format_ident!("{}MissingError", ident)),
_ => None,
}
}
pub fn try_parse(
&self,
idx: usize,
error_enum_name: &Ident,
error_enum_varient: &Ident,
inner_parse_enum: &Ident,
parse_children: TokenStream2,
) -> TokenStream2 {
let error_name = self.error_name(idx);
match self {
Self::Static(segment) => {
quote! {
{
let mut segments = segments.clone();
let segment = segments.next();
let parsed = if let Some(#segment) = segment {
Ok(())
} else {
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(segment.map(|s|s.to_string()).unwrap_or_default())))
};
match parsed {
Ok(_) => {
#parse_children
}
Err(err) => {
errors.push(err);
}
}
}
}
}
Self::Dynamic(name, ty) => {
let missing_error_name = self.missing_error_name().unwrap();
quote! {
{
let mut segments = segments.clone();
let parsed = if let Some(segment) = segments.next() {
<#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
} else {
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
};
match parsed {
Ok(#name) => {
#parse_children
}
Err(err) => {
errors.push(err);
}
}
}
}
}
Self::CatchAll(name, ty) => {
quote! {
{
let parsed = {
let mut segments = segments.clone();
let segments: Vec<_> = segments.collect();
<#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
};
match parsed {
Ok(#name) => {
#parse_children
}
Err(err) => {
errors.push(err);
}
}
}
}
}
}
}
}
pub fn static_segment_idx(idx: usize) -> Ident {
format_ident!("StaticSegment{}ParseError", idx)
}
pub fn parse_route_segments<'a>(
route_span: Span,
mut fields: impl Iterator<Item = (&'a Ident, &'a Type)>,
route: &str,
) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
let mut route_segments = Vec::new();
let (route_string, query) = match route.rsplit_once('?') {
Some((route, query)) => (route, Some(query)),
None => (route, None),
};
let mut iterator = route_string.split('/');
// skip the first empty segment
let first = iterator.next();
if first != Some("") {
return Err(syn::Error::new(
route_span,
format!(
"Routes should start with /. Error found in the route '{}'",
route
),
));
}
while let Some(segment) = iterator.next() {
if let Some(segment) = segment.strip_prefix(':') {
let spread = segment.starts_with("..");
let ident = if spread {
segment[2..].to_string()
} else {
segment.to_string()
};
let field = fields.find(|(name, _)| **name == ident);
let ty = if let Some(field) = field {
field.1.clone()
} else {
return Err(syn::Error::new(
route_span,
format!("Could not find a field with the name '{}'", ident,),
));
};
if spread {
route_segments.push(RouteSegment::CatchAll(
Ident::new(&ident, Span::call_site()),
ty,
));
if iterator.next().is_some() {
return Err(syn::Error::new(
route_span,
"Catch-all route segments must be the last segment in a route. The route segments after the catch-all segment will never be matched.",
));
} else {
break;
}
} else {
route_segments.push(RouteSegment::Dynamic(
Ident::new(&ident, Span::call_site()),
ty,
));
}
} else {
route_segments.push(RouteSegment::Static(segment.to_string()));
}
}
// check if the route has a query string
let parsed_query = match query {
Some(query) => {
if let Some(query) = query.strip_prefix(':') {
let query_ident = Ident::new(query, Span::call_site());
let field = fields.find(|(name, _)| *name == &query_ident);
let ty = if let Some((_, ty)) = field {
ty.clone()
} else {
return Err(syn::Error::new(
route_span,
format!("Could not find a field with the name '{}'", query_ident),
));
};
Some(QuerySegment {
ident: query_ident,
ty,
})
} else {
None
}
}
None => None,
};
Ok((route_segments, parsed_query))
}
pub(crate) fn create_error_type(
error_name: Ident,
segments: &[RouteSegment],
child_type: Option<&Type>,
) -> TokenStream2 {
let mut error_variants = Vec::new();
let mut display_match = Vec::new();
for (i, segment) in segments.iter().enumerate() {
let error_name = segment.error_name(i);
match segment {
RouteSegment::Static(index) => {
error_variants.push(quote! { #error_name(String) });
display_match.push(quote! { Self::#error_name(found) => write!(f, "Static segment '{}' did not match instead found '{found}'", #index)? });
}
RouteSegment::Dynamic(ident, ty) => {
let missing_error = segment.missing_error_name().unwrap();
error_variants.push(
quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) },
);
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
error_variants.push(quote! { #missing_error });
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
}
RouteSegment::CatchAll(ident, ty) => {
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
}
}
}
let child_type_variant = child_type
.map(|child_type| {
quote! { ChildRoute(<#child_type as std::str::FromStr>::Err) }
})
.into_iter();
let child_type_error = child_type
.map(|_| {
quote! {
Self::ChildRoute(error) => {
write!(f, "{}", error)?
}
}
})
.into_iter();
quote! {
#[allow(non_camel_case_types)]
#[derive(Debug, PartialEq)]
pub enum #error_name {
ExtraSegments(String),
#(#child_type_variant,)*
#(#error_variants,)*
}
impl std::fmt::Display for #error_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExtraSegments(segments) => {
write!(f, "Found additional trailing segments: {}", segments)?
},
#(#child_type_error,)*
#(#display_match,)*
}
Ok(())
}
}
}
}
+1
View File
@@ -1 +1,2 @@
dist/
static
+38 -40
View File
@@ -9,51 +9,49 @@ homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus = { workspace = true}
futures-channel = { workspace = true }
url = { version = "2.2.2", default-features = false }
# for wasm
web-sys = { version = "0.3", features = [
"Attr",
"Document",
"History",
"HtmlBaseElement",
"Event",
"NamedNodeMap",
"Url",
"UrlSearchParams",
"Window",
"Location",
], optional = true }
wasm-bindgen = { workspace = true, optional = true }
js-sys = { version = "0.3", optional = true }
gloo-events = { version = "0.1.1", optional = true }
anyhow = "1.0.66"
dioxus = { workspace = true }
dioxus-router-macro = { workspace = true }
gloo = { version = "0.8.0", optional = true }
log = { workspace = true }
thiserror = "1.0.30"
futures-util = { workspace = true }
serde = { version = "1", optional = true }
serde_urlencoded = { version = "0.7.1", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
thiserror = "1.0.37"
url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"] }
js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
dioxus-ssr = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
[features]
default = ["query"]
web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"]
query = ["serde", "serde_urlencoded"]
default = ["web"]
ssr = ["dioxus-ssr", "tokio"]
wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies]
dioxus = { path = "../dioxus" }
dioxus-ssr = { path = "../ssr" }
criterion = { verison = "0.5", features = ["async_tokio", "html_reports"] }
[[example]]
name = "static_generation"
required-features = ["ssr"]
[[bench]]
name = "incremental"
required-features = ["ssr"]
harness = false
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
dioxus-desktop = { path = "../desktop" }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_error_panic_hook = "0.1.7"
wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"
dioxus-web = { workspace = true }
# dioxus-desktop = { workspace = true, optional = true }
# not wasm
[target.wasm32-unknown-unknown.dev-dependencies]
dioxus-router = { workspace = true, features = ["web"] }
[dev-dependencies.web-sys]
version = "0.3"
features = ["Document"]
dioxus-web = { workspace = true }
gloo = "0.8.0"
wasm-bindgen-test = "0.3.33"
wasm-logger = "0.2.0"
-10
View File
@@ -1,10 +0,0 @@
[tasks.test-with-browser]
extend = "core::wasm-pack-base"
command = "wasm-pack"
args = [
"test",
"@@split(DIOXUS_TEST_FLAGS, )",
"--",
"--features",
"${DIOXUS_TEST_FEATURES}",
]
+73 -15
View File
@@ -7,35 +7,93 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-router.svg
[crates-url]: https://crates.io/crates/dioxus-router
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/docs/0.3/router/) |
[API Docs](https://docs.rs/dioxus-router/latest/dioxus_router) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router-style interface using somewhat loose typing rules.
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides an
interface similar to React Router, but takes advantage of types for more
expressiveness.
```rust, ignore
fn app() {
cx.render(rsx! {
Router {
Route { to: "/", Component {} },
Route { to: "/blog", Blog {} },
Route { to: "/blog/:id", BlogPost {} },
```rust, no_run
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[layout(Blog)]
#[route("/")]
BlogList {},
#[route("/:blog_id")]
BlogPost { blog_id: usize },
#[end_layout]
#[end_nest]
#[route("/")]
Index {},
}
fn App(cx: Scope) -> Element {
render! {
Router { }
}
}
#[inline_props]
fn Index(cx: Scope) -> Element {
render! {
h1 { "Index" }
Link {
target: Route::BlogList {},
"Go to the blog"
}
})
}
}
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
h1 { "Blog" }
Outlet { }
}
}
#[inline_props]
fn BlogList(cx: Scope) -> Element {
render! {
h2 { "List of blog posts" }
Link {
target: Route::BlogPost { blog_id: 0 },
"Blog post 1"
}
Link {
target: Route::BlogPost { blog_id: 1 },
"Blog post 2"
}
}
}
#[inline_props]
fn BlogPost(cx: Scope, blog_id: usize) -> Element {
render! {
h2 { "Blog Post" }
}
}
```
@@ -47,6 +105,7 @@ You need to enable the right features for the platform you're targeting since th
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
@@ -54,4 +113,3 @@ This project is licensed under the [MIT license].
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+200
View File
@@ -0,0 +1,200 @@
#![allow(unused, non_snake_case)]
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dioxus_ssr::Renderer;
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("build 1000 routes", |b| {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
b.iter(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
)
.await
.unwrap();
}
})
})
});
c.bench_function("build 1000 routes no memory cache", |b| {
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.memory_cache_limit(0)
.invalidate_after(Duration::from_secs(10))
.build();
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
)
.await
.unwrap();
}
})
});
c.bench_function("build 1000 routes no cache", |b| {
let mut renderer = Renderer::default();
b.iter(|| {
for id in 0..1000 {
let mut vdom = VirtualDom::new_with_props(
RenderPath,
RenderPathProps::builder().path(Route::Post { id }).build(),
);
vdom.rebuild();
struct Ignore;
impl std::fmt::Write for Ignore {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
Ok(())
}
}
renderer.render_to(&mut Ignore, &vdom).unwrap();
}
})
});
c.bench_function("cache static", |b| {
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.build();
pre_cache_static_routes::<Route, _>(&mut renderer)
.await
.unwrap();
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
div {
"Blog"
}
}
}
#[inline_props]
fn Post(cx: Scope, id: usize) -> Element {
render! {
for _ in 0..*id {
div {
"PostId: {id}"
}
}
}
}
#[inline_props]
fn PostHome(cx: Scope) -> Element {
render! {
div {
"Post"
}
}
}
#[inline_props]
fn Home(cx: Scope) -> Element {
render! {
div {
"Home"
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[route("/")]
Blog {},
#[route("/post/index")]
PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest]
#[route("/")]
Home {},
}
#[inline_props]
fn RenderPath(cx: Scope, path: Route) -> Element {
let path = path.clone();
render! {
Router {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}
+141 -41
View File
@@ -1,52 +1,152 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
fn main() {
dioxus_web::launch(app);
#[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch(root);
#[cfg(target_arch = "wasm32")]
dioxus_web::launch(root);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Router {
h1 { "Your app here" }
ul {
Link { to: "/", li { "home" } }
Link { to: "/blog", li { "blog" } }
Link { to: "/blog/tim", li { "tims' blog" } }
Link { to: "/blog/bill", li { "bills' blog" } }
Link { to: "/blog/james", li { "james amazing' blog" } }
Link { to: "/apples", li { "go to apples" } }
}
Route { to: "/", Home {} }
Route { to: "/blog/", BlogList {} }
Route { to: "/blog/:id/", BlogPost {} }
Route { to: "/oranges", "Oranges are not apples!" }
Redirect { from: "/apples", to: "/oranges" }
fn root(cx: Scope) -> Element {
render! {
Router {}
}
}
#[inline_props]
fn UserFrame(cx: Scope, user_id: usize) -> Element {
render! {
pre {
"UserFrame{{\n\tuser_id:{user_id}\n}}"
}
})
}
fn Home(cx: Scope) -> Element {
cx.render(rsx! { h1 { "Home" } })
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! { div { "Blog List" } })
}
fn BlogPost(cx: Scope) -> Element {
let Some(id) = use_route(cx).segment("id") else {
return cx.render(rsx! { div { "No blog post id" } });
};
log::trace!("rendering blog post {}", id);
cx.render(rsx! {
div {
h3 { "blog post: {id:?}" }
Link { to: "/blog/", "back to blog list" }
background_color: "rgba(0,0,0,50%)",
"children:"
Outlet {}
}
})
}
}
#[inline_props]
fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: String) -> Element {
render! {
pre {
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}"
}
Link {
target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." },
"Route1 with extra+\".\""
}
p { "Footer" }
Link {
target: Route::Route3 { dynamic: String::new() },
"Home"
}
}
}
#[inline_props]
fn Route2(cx: Scope, user_id: usize) -> Element {
render! {
pre {
"Route2{{\n\tuser_id:{user_id}\n}}"
}
(0..*user_id).map(|i| rsx!{ p { "{i}" } }),
p { "Footer" }
Link {
target: Route::Route3 { dynamic: String::new() },
"Home"
}
}
}
#[inline_props]
fn Route3(cx: Scope, dynamic: String) -> Element {
let navigator = use_navigator(cx);
let current_route = use_route(cx)?;
let current_route_str = use_ref(cx, String::new);
let parsed = Route::from_str(&current_route_str.read());
let site_map = Route::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
.collect::<Vec<_>>();
render! {
input {
oninput: move |evt| {
*current_route_str.write() = evt.value.clone();
},
value: "{current_route_str.read()}"
}
"dynamic: {dynamic}"
Link {
target: Route::Route2 { user_id: 8888 },
"hello world link"
}
button {
onclick: move |_| { navigator.push(NavigationTarget::External("https://www.google.com".to_string())); },
"google link"
}
p { "Site Map" }
pre { "{site_map:#?}" }
p { "Dynamic link" }
match parsed {
Ok(route) => {
if route != current_route {
render! {
Link {
target: route.clone(),
"{route}"
}
}
}
else {
None
}
}
Err(err) => {
render! {
pre {
color: "red",
"Invalid route:\n{err}"
}
}
}
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/test")]
// Nests with parameters have types taken from child routes
#[nest("/user/:user_id")]
// Everything inside the nest has the added parameter `user_id: usize`
// UserFrame is a layout component that will receive the `user_id: usize` parameter
#[layout(UserFrame)]
#[route("/:dynamic?:query")]
Route1 {
// The type is taken from the first instance of the dynamic parameter
user_id: usize,
dynamic: usize,
query: String,
extra: String,
},
#[route("/hello_world")]
// You can opt out of the layout by using the `!` prefix
#[layout(!UserFrame)]
Route2 { user_id: usize },
#[end_layout]
#[end_nest]
#[end_nest]
#[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
#[route("/:dynamic")]
Route3 { dynamic: String },
}
@@ -0,0 +1,103 @@
#![allow(non_snake_case)]
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use dioxus_ssr::incremental::{DefaultRenderer, IncrementalRendererConfig};
#[tokio::main]
async fn main() {
let mut renderer = IncrementalRendererConfig::new(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
println!(
"SITE MAP:\n{}",
Route::SITE_MAP
.iter()
.flat_map(|route| route.flatten().into_iter())
.map(|route| {
route
.iter()
.map(|segment| segment.to_string())
.collect::<Vec<_>>()
.join("")
})
.collect::<Vec<_>>()
.join("\n")
);
pre_cache_static_routes::<Route, _>(&mut renderer)
.await
.unwrap();
}
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
div {
"Blog"
}
}
}
#[inline_props]
fn Post(cx: Scope, id: usize) -> Element {
render! {
div {
"PostId: {id}"
}
}
}
#[inline_props]
fn PostHome(cx: Scope) -> Element {
render! {
div {
"Post"
}
}
}
#[inline_props]
fn Home(cx: Scope) -> Element {
render! {
div {
"Home"
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[route("/")]
Blog {},
#[route("/post/index")]
PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest]
#[route("/")]
Home {},
}
-6
View File
@@ -1,6 +0,0 @@
#[derive(Default)]
pub struct RouterCfg {
pub base_url: Option<String>,
pub active_class: Option<String>,
pub initial_url: Option<String>,
}
Loaded 100 of 236 files, more files were not shown because too many files have changed in this diff. Show more