Merge branch 'master' into jk/remove-dioxus-bin
This commit is contained in:
259 files changed
+11706
-4788
No files matched your search
@@ -48,7 +48,6 @@ impl Writer<'_> {
|
||||
key,
|
||||
attributes,
|
||||
children,
|
||||
_is_static,
|
||||
brace,
|
||||
} = el;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
You can use this command to build a project:
|
||||
@@ -37,6 +38,14 @@ dx build --platform desktop
|
||||
dx build --platform web
|
||||
```
|
||||
|
||||
## Specify workspace bin
|
||||
|
||||
You can add the `--bin` option to select which crate you want Dioxus to build:
|
||||
|
||||
```
|
||||
dioxus build --bin app
|
||||
```
|
||||
|
||||
## Build Example
|
||||
|
||||
You can use the `example` option to select a example to build:
|
||||
|
||||
@@ -7,7 +7,10 @@ dioxus-clean
|
||||
Clean build artifacts
|
||||
|
||||
USAGE:
|
||||
dx clean
|
||||
dx clean [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
# Example
|
||||
@@ -16,3 +19,9 @@ USAGE:
|
||||
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
|
||||
```
|
||||
@@ -13,7 +13,8 @@ OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
--hot-reload [default: false]ß
|
||||
--hot-reload [default: false]
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
You can use this command to build project and start a dev server:
|
||||
@@ -31,6 +32,14 @@ You can use the `example` option to serve a example:
|
||||
dx serve --exmaple test
|
||||
```
|
||||
|
||||
## Specify workspace bin
|
||||
|
||||
You can add the `--bin` option to select which crate you want Dioxus to build and serve:
|
||||
|
||||
```
|
||||
dioxus serve --bin app
|
||||
```
|
||||
|
||||
## Open Browser
|
||||
|
||||
You can add the `--open` option to open system default browser when server startup:
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Autoformat {
|
||||
///
|
||||
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
|
||||
async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
let crate_config = crate::CrateConfig::new(None)?;
|
||||
|
||||
let mut files_to_format = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||
@@ -136,12 +136,16 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else { return };
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
@@ -12,8 +12,8 @@ pub struct Build {
|
||||
}
|
||||
|
||||
impl Build {
|
||||
pub fn build(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
pub fn build(self, bin: Option<PathBuf>) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new(bin)?;
|
||||
|
||||
// change the release state.
|
||||
crate_config.with_release(self.build.release);
|
||||
@@ -6,8 +6,8 @@ use super::*;
|
||||
pub struct Clean {}
|
||||
|
||||
impl Clean {
|
||||
pub fn clean(self) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
pub fn clean(self, bin: Option<PathBuf>) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new(bin)?;
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.arg("clean")
|
||||
@@ -41,19 +41,19 @@ impl Config {
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = File::create(conf_path)?;
|
||||
let content = String::from(include_str!("../../assets/dioxus.toml"))
|
||||
let content = String::from(include_str!("../assets/dioxus.toml"))
|
||||
.replace("{{project-name}}", &name)
|
||||
.replace("{{default-platform}}", &platform);
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Init config file completed.");
|
||||
}
|
||||
Config::FormatPrint {} => {
|
||||
println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
|
||||
println!("{:#?}", crate::CrateConfig::new(None)?.dioxus_config);
|
||||
}
|
||||
Config::CustomHtml {} => {
|
||||
let html_path = crate_root.join("index.html");
|
||||
let mut file = File::create(html_path)?;
|
||||
let content = include_str!("../../assets/index.html");
|
||||
let content = include_str!("../assets/index.html");
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Create custom html file done.");
|
||||
}
|
||||
File renamed without changes.
@@ -36,6 +36,10 @@ pub struct Cli {
|
||||
/// Enable verbose logging.
|
||||
#[clap(short)]
|
||||
pub v: bool,
|
||||
|
||||
/// Specify bin target
|
||||
#[clap(global = true, long)]
|
||||
pub bin: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
|
||||
File renamed without changes.
@@ -15,8 +15,8 @@ pub struct Serve {
|
||||
}
|
||||
|
||||
impl Serve {
|
||||
pub async fn serve(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new(bin)?;
|
||||
|
||||
// change the relase state.
|
||||
crate_config.with_hot_reload(self.serve.hot_reload);
|
||||
File renamed without changes.
@@ -130,7 +130,7 @@ fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String>
|
||||
|
||||
#[test]
|
||||
fn generates_svgs() {
|
||||
let st = include_str!("../../../tests/svg.html");
|
||||
let st = include_str!("../../tests/svg.html");
|
||||
|
||||
let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true);
|
||||
|
||||
+49
-19
@@ -1,6 +1,9 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DioxusConfig {
|
||||
@@ -17,29 +20,49 @@ fn default_plugin() -> toml::Value {
|
||||
}
|
||||
|
||||
impl DioxusConfig {
|
||||
pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
|
||||
let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
|
||||
pub fn load(bin: Option<PathBuf>) -> crate::error::Result<Option<DioxusConfig>> {
|
||||
let crate_dir = crate::cargo::crate_root();
|
||||
|
||||
let crate_dir = match crate_dir {
|
||||
Ok(dir) => {
|
||||
if let Some(bin) = bin {
|
||||
dir.join(bin)
|
||||
} else {
|
||||
dir
|
||||
}
|
||||
}
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
let crate_dir = crate_dir.as_path();
|
||||
|
||||
// we support either `Dioxus.toml` or `Cargo.toml`
|
||||
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let dioxus_conf_file = dioxus_conf_file.as_path();
|
||||
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
|
||||
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
|
||||
.map_err(|err| {
|
||||
let error_location = dioxus_conf_file
|
||||
.strip_prefix(crate_dir)
|
||||
.unwrap_or(dioxus_conf_file)
|
||||
.display();
|
||||
crate::Error::Unique(format!("{error_location} {err}"))
|
||||
})
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
|
||||
fn acquire_dioxus_toml(dir: &Path) -> Option<PathBuf> {
|
||||
// prefer uppercase
|
||||
if dir.join("Dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
let uppercase_conf = dir.join("Dioxus.toml");
|
||||
if uppercase_conf.is_file() {
|
||||
return Some(uppercase_conf);
|
||||
}
|
||||
|
||||
// lowercase is fine too
|
||||
if dir.join("dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
let lowercase_conf = dir.join("dioxus.toml");
|
||||
if lowercase_conf.is_file() {
|
||||
return Some(lowercase_conf);
|
||||
}
|
||||
|
||||
None
|
||||
@@ -176,14 +199,19 @@ pub enum ExecutableType {
|
||||
}
|
||||
|
||||
impl CrateConfig {
|
||||
pub fn new() -> Result<Self> {
|
||||
let dioxus_config = DioxusConfig::load()?.unwrap_or_default();
|
||||
pub fn new(bin: Option<PathBuf>) -> Result<Self> {
|
||||
let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default();
|
||||
|
||||
let crate_root = crate::cargo::crate_root()?;
|
||||
|
||||
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
|
||||
crate::cargo::crate_root()?.join(package)
|
||||
crate_root.join(package)
|
||||
} else if let Some(bin) = bin {
|
||||
crate_root.join(bin)
|
||||
} else {
|
||||
crate::cargo::crate_root()?
|
||||
crate_root
|
||||
};
|
||||
|
||||
let meta = crate::cargo::Metadata::get()?;
|
||||
let workspace_dir = meta.workspace_root;
|
||||
let target_dir = meta.target_directory;
|
||||
@@ -202,8 +230,9 @@ impl CrateConfig {
|
||||
|
||||
let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap();
|
||||
|
||||
let output_filename = {
|
||||
match &manifest.package.as_ref().unwrap().default_run {
|
||||
let mut output_filename = String::from("dioxus_app");
|
||||
if let Some(package) = &manifest.package.as_ref() {
|
||||
output_filename = match &package.default_run {
|
||||
Some(default_run_target) => default_run_target.to_owned(),
|
||||
None => manifest
|
||||
.bin
|
||||
@@ -216,9 +245,10 @@ impl CrateConfig {
|
||||
.or(manifest.bin.first())
|
||||
.or(manifest.lib.as_ref())
|
||||
.and_then(|prod| prod.name.clone())
|
||||
.expect("No executable or library found from cargo metadata."),
|
||||
}
|
||||
};
|
||||
.unwrap_or(String::from("dioxus_app")),
|
||||
};
|
||||
}
|
||||
|
||||
let executable = ExecutableType::Binary(output_filename);
|
||||
|
||||
let release = false;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use dioxus_cli::*;
|
||||
@@ -7,14 +9,54 @@ use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
|
||||
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
|
||||
|
||||
if let Some(ref bin) = bin {
|
||||
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
|
||||
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
|
||||
|
||||
if let Some(workspace) = manifest.workspace {
|
||||
for item in workspace.members.iter() {
|
||||
let path = PathBuf::from(item);
|
||||
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.ends_with(bin.clone()) {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// If the bin exists but we couldn't find it
|
||||
if bin.is_some() {
|
||||
return Err(Error::CargoError(
|
||||
"The specified bin does not exist.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
||||
set_up_logging();
|
||||
|
||||
let _dioxus_config = DioxusConfig::load()
|
||||
.map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
|
||||
let bin = get_bin(args.bin)?;
|
||||
|
||||
let _dioxus_config = DioxusConfig::load(bin.clone())
|
||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
DioxusConfig::default()
|
||||
@@ -30,15 +72,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build()
|
||||
.build(bin.clone())
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean()
|
||||
.clean(bin.clone())
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve()
|
||||
.serve(bin.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
@@ -445,7 +444,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 +501,6 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
component: &'b VComponent<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
use RenderReturn::*;
|
||||
|
||||
@@ -512,9 +510,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 +528,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 +535,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>,
|
||||
|
||||
@@ -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,9 +124,8 @@ 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
|
||||
@@ -160,13 +144,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 +173,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 +180,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 +217,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 +278,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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +715,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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,7 +916,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
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> {
|
||||
@@ -688,32 +684,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) }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 matches!(allocated, RenderReturn::Aborted(_)) {
|
||||
if scope.suspended.get() {
|
||||
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() }
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
@@ -688,13 +693,13 @@ impl<'src> ScopeState {
|
||||
raw_ref.downcast_mut::<State>()
|
||||
})
|
||||
.expect(
|
||||
r###"
|
||||
r#"
|
||||
Unable to retrieve the hook that was initialized at this index.
|
||||
Consult the `rules of hooks` to understand how to use hooks properly.
|
||||
|
||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||
Functions prefixed with "use" should never be called conditionally.
|
||||
"###,
|
||||
"#,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,21 @@ 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 {
|
||||
if self.suspended_scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.wait_for_work().await;
|
||||
|
||||
_ = 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 +592,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 +602,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 +612,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();
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
+10
-10
@@ -1,4 +1,3 @@
|
||||
|
||||
<div align="center">
|
||||
<h1>Fermi ⚛</h1>
|
||||
<p>
|
||||
@@ -6,7 +5,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<!-- Crates version -->
|
||||
<a href="https://crates.io/crates/fermi">
|
||||
@@ -30,21 +28,21 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
Fermi is a global state management solution for Dioxus that's as easy as `use_state`.
|
||||
|
||||
Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`:
|
||||
|
||||
```rust, ignore
|
||||
static NAME: Atom<&str> = |_| "Dioxus";
|
||||
static NAME: Atom<&str> = Atom(|_| "Dioxus");
|
||||
```
|
||||
|
||||
From anywhere in our app, we can read the value of our atom:
|
||||
|
||||
```rust, ignores
|
||||
```rust, ignore
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let name = use_read(cx, NAME);
|
||||
let name = use_read(cx, &NAME);
|
||||
cx.render(rsx!{ h1 { "Hello, {name}"} })
|
||||
}
|
||||
```
|
||||
@@ -53,7 +51,7 @@ We can also set the value of our atom, also from anywhere in our app:
|
||||
|
||||
```rust, ignore
|
||||
fn NameCard(cx: Scope) -> Element {
|
||||
let set_name = use_set(cx, NAME);
|
||||
let set_name = use_set(cx, &NAME);
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
onclick: move |_| set_name("Fermi"),
|
||||
@@ -66,10 +64,10 @@ fn NameCard(cx: Scope) -> Element {
|
||||
If needed, we can update the atom's value, based on itself:
|
||||
|
||||
```rust, ignore
|
||||
static COUNT: Atom<i32> = |_| 0;
|
||||
static COUNT: Atom<i32> = Atom(|_| 0);
|
||||
|
||||
fn Counter(cx: Scope) -> Element {
|
||||
let mut count = use_atom_state(cx, COUNT);
|
||||
let mut count = use_atom_state(cx, &COUNT);
|
||||
|
||||
cx.render(rsx!{
|
||||
p {
|
||||
@@ -86,6 +84,7 @@ fn Counter(cx: Scope) -> Element {
|
||||
It's that simple!
|
||||
|
||||
## Installation
|
||||
|
||||
Fermi is currently under construction, so you have to use the `master` branch to get started.
|
||||
|
||||
```toml
|
||||
@@ -93,10 +92,10 @@ Fermi is currently under construction, so you have to use the `master` branch to
|
||||
fermi = { git = "https://github.com/dioxuslabs/dioxus" }
|
||||
```
|
||||
|
||||
|
||||
## Running examples
|
||||
|
||||
The examples here use Dioxus Desktop to showcase their functionality. To run an example, use
|
||||
|
||||
```sh
|
||||
$ cargo run --example fermi
|
||||
```
|
||||
@@ -104,6 +103,7 @@ $ cargo run --example fermi
|
||||
## Features
|
||||
|
||||
Broadly our feature set required to be released includes:
|
||||
|
||||
- [x] Support for Atoms
|
||||
- [x] Support for AtomRef (for values that aren't `Clone`)
|
||||
- [ ] Support for Atom Families
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use crate::{AtomId, AtomRoot, Readable, Writable};
|
||||
|
||||
pub type Atom<T> = fn(AtomBuilder) -> T;
|
||||
pub struct Atom<T>(pub fn(AtomBuilder) -> T);
|
||||
pub struct AtomBuilder;
|
||||
|
||||
impl<V: 'static> Readable<V> for Atom<V> {
|
||||
impl<V> Readable<V> for &'static Atom<V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<V> {
|
||||
todo!()
|
||||
}
|
||||
fn init(&self) -> V {
|
||||
(*self)(AtomBuilder)
|
||||
self.0(AtomBuilder)
|
||||
}
|
||||
fn unique_id(&self) -> AtomId {
|
||||
AtomId {
|
||||
ptr: *self as *const (),
|
||||
type_id: std::any::TypeId::of::<V>(),
|
||||
}
|
||||
*self as *const Atom<V> as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> Writable<V> for Atom<V> {
|
||||
impl<V> Writable<V> for &'static Atom<V> {
|
||||
fn write(&self, _root: AtomRoot, _value: V) {
|
||||
todo!()
|
||||
}
|
||||
@@ -26,6 +23,22 @@ impl<V: 'static> Writable<V> for Atom<V> {
|
||||
|
||||
#[test]
|
||||
fn atom_compiles() {
|
||||
static TEST_ATOM: Atom<&str> = |_| "hello";
|
||||
dbg!(TEST_ATOM.init());
|
||||
static TEST_ATOM: Atom<&str> = Atom(|_| "hello");
|
||||
dbg!((&TEST_ATOM).init());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atom_is_unique() {
|
||||
static TEST_ATOM_1: Atom<&str> = Atom(|_| "hello");
|
||||
static TEST_ATOM_2: Atom<&str> = Atom(|_| "hello");
|
||||
assert_eq!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_1).unique_id());
|
||||
assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atom_is_unique_2() {
|
||||
struct S(String);
|
||||
static TEST_ATOM_1: Atom<Vec<S>> = Atom(|_| Vec::new());
|
||||
static TEST_ATOM_2: Atom<Vec<String>> = Atom(|_| Vec::new());
|
||||
assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id());
|
||||
}
|
||||
@@ -2,26 +2,23 @@ use crate::{AtomId, AtomRoot, Readable, Writable};
|
||||
use im_rc::HashMap as ImMap;
|
||||
|
||||
pub struct AtomFamilyBuilder;
|
||||
pub type AtomFamily<K, V> = fn(AtomFamilyBuilder) -> ImMap<K, V>;
|
||||
pub struct AtomFamily<K, V>(pub fn(AtomFamilyBuilder) -> ImMap<K, V>);
|
||||
|
||||
impl<K, V: 'static> Readable<ImMap<K, V>> for AtomFamily<K, V> {
|
||||
impl<K, V> Readable<ImMap<K, V>> for &'static AtomFamily<K, V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn init(&self) -> ImMap<K, V> {
|
||||
(*self)(AtomFamilyBuilder)
|
||||
self.0(AtomFamilyBuilder)
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> AtomId {
|
||||
AtomId {
|
||||
ptr: *self as *const (),
|
||||
type_id: std::any::TypeId::of::<V>(),
|
||||
}
|
||||
*self as *const AtomFamily<K, V> as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V: 'static> Writable<ImMap<K, V>> for AtomFamily<K, V> {
|
||||
impl<K, V> Writable<ImMap<K, V>> for &'static AtomFamily<K, V> {
|
||||
fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -2,27 +2,24 @@ use crate::{AtomId, AtomRoot, Readable};
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct AtomRefBuilder;
|
||||
pub type AtomRef<T> = fn(AtomRefBuilder) -> T;
|
||||
pub struct AtomRef<T>(pub fn(AtomRefBuilder) -> T);
|
||||
|
||||
impl<V: 'static> Readable<RefCell<V>> for AtomRef<V> {
|
||||
impl<V> Readable<RefCell<V>> for &'static AtomRef<V> {
|
||||
fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn init(&self) -> RefCell<V> {
|
||||
RefCell::new((*self)(AtomRefBuilder))
|
||||
RefCell::new(self.0(AtomRefBuilder))
|
||||
}
|
||||
|
||||
fn unique_id(&self) -> AtomId {
|
||||
AtomId {
|
||||
ptr: *self as *const (),
|
||||
type_id: std::any::TypeId::of::<V>(),
|
||||
}
|
||||
*self as *const AtomRef<V> as *const ()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atom_compiles() {
|
||||
static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![];
|
||||
dbg!(TEST_ATOM.init());
|
||||
static TEST_ATOM: AtomRef<Vec<String>> = AtomRef(|_| vec![]);
|
||||
dbg!((&TEST_ATOM).init());
|
||||
}
|
||||
@@ -13,7 +13,10 @@ use std::{
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn use_atom_ref<T: 'static>(cx: &ScopeState, atom: AtomRef<T>) -> &UseAtomRef<T> {
|
||||
pub fn use_atom_ref<'a, T: 'static>(
|
||||
cx: &'a ScopeState,
|
||||
atom: &'static AtomRef<T>,
|
||||
) -> &'a UseAtomRef<T> {
|
||||
let root = use_atom_root(cx);
|
||||
|
||||
&cx.use_hook(|| {
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::{
|
||||
/// static COUNT: Atom<u32> = |_| 0;
|
||||
///
|
||||
/// fn Example(cx: Scope) -> Element {
|
||||
/// let mut count = use_atom_state(cx, COUNT);
|
||||
/// let mut count = use_atom_state(cx, &COUNT);
|
||||
///
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
|
||||
use dioxus_core::ScopeId;
|
||||
use im_rc::HashSet;
|
||||
|
||||
use crate::Readable;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct AtomId {
|
||||
pub ptr: *const (),
|
||||
pub type_id: TypeId,
|
||||
}
|
||||
pub type AtomId = *const ();
|
||||
|
||||
pub struct AtomRoot {
|
||||
pub atoms: RefCell<HashMap<AtomId, Slot>>,
|
||||
@@ -54,15 +44,7 @@ impl AtomRoot {
|
||||
// initialize the value if it's not already initialized
|
||||
if let Some(slot) = atoms.get_mut(&f.unique_id()) {
|
||||
slot.subscribers.insert(scope);
|
||||
match slot.value.clone().downcast() {
|
||||
Ok(res) => res,
|
||||
Err(e) => panic!(
|
||||
"Downcasting atom failed: {:?}. Has typeid of {:?} but needs typeid of {:?}",
|
||||
f.unique_id(),
|
||||
e.type_id(),
|
||||
TypeId::of::<V>()
|
||||
),
|
||||
}
|
||||
slot.value.clone().downcast().unwrap()
|
||||
} else {
|
||||
let value = Rc::new(f.init());
|
||||
let mut subscribers = HashSet::new();
|
||||
|
||||
@@ -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
|
||||
//! dioxus 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(¤t_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
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dioxus 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("dx 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
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dioxus 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("dx 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
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dioxus 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("dx 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
|
||||
//! dioxus 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
|
||||
//! dx build --features web
|
||||
//! cargo run --features ssr --no-default-features
|
||||
//! dioxus 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("dx 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());
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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())?
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))]
|
||||
{
|
||||
|
||||
@@ -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>>>,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
@@ -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<()> {}
|
||||
@@ -9,17 +9,33 @@ use crate::UseFutureDep;
|
||||
/// If a future is pending when the dependencies change, the previous future
|
||||
/// will be allowed to continue
|
||||
///
|
||||
/// - dependencies: a tuple of references to values that are PartialEq + Clone
|
||||
/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, name: &str) -> Element {
|
||||
/// use_effect(cx, (name,), |(name,)| async move {
|
||||
/// set_title(name);
|
||||
/// }))
|
||||
/// fn Profile(cx: Scope, id: usize) -> Element {
|
||||
/// let name = use_state(cx, || None);
|
||||
///
|
||||
/// // Only fetch the user data when the id changes.
|
||||
/// use_effect(cx, (id,), |(id,)| {
|
||||
/// to_owned![name];
|
||||
/// async move {
|
||||
/// let user = fetch_user(id).await;
|
||||
/// name.set(user.name);
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// let name = name.get().clone().unwrap_or("Loading...".to_string());
|
||||
///
|
||||
/// render!(
|
||||
/// p { "{name}" }
|
||||
/// )
|
||||
/// }
|
||||
///
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
/// render!(Profile { id: 0 })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_effect<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)
|
||||
|
||||
@@ -2,21 +2,27 @@ use dioxus_core::ScopeState;
|
||||
|
||||
use crate::UseFutureDep;
|
||||
|
||||
/// A hook that provides a callback that executes after the hooks have been applied
|
||||
/// A hook that provides a callback that executes if the dependencies change.
|
||||
/// This is useful to avoid running computation-expensive calculations even when the data doesn't change.
|
||||
///
|
||||
/// Whenever the hooks dependencies change, the callback will be re-evaluated.
|
||||
///
|
||||
/// - dependencies: a tuple of references to values that are PartialEq + Clone
|
||||
/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, name: &str) -> Element {
|
||||
/// use_memo(cx, (name,), |(name,)| {
|
||||
/// expensive_computation(name);
|
||||
/// }))
|
||||
/// fn Calculator(cx: Scope, number: usize) -> Element {
|
||||
/// let bigger_number = use_memo(cx, (number,), |(number,)| {
|
||||
/// // This will only be calculated when `number` has changed.
|
||||
/// number * 100
|
||||
/// });
|
||||
/// render!(
|
||||
/// p { "{bigger_number}" }
|
||||
/// )
|
||||
/// }
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
/// render!(Calculator { number: 0 })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_memo<T, D>(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{
|
||||
/// writes through the `write` method. Whenever `write` is called, the component
|
||||
/// that initialized the hook will be marked as "dirty".
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// // using `write` will give us a `RefMut` to the inner value, which we can call methods on
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
///
|
||||
/// You can avoid this default behavior with `write_silent`
|
||||
///
|
||||
/// ```ignore
|
||||
/// ```rust, no_run
|
||||
/// // with `write_silent`, the component will not be re-rendered
|
||||
/// val.write_silent().insert(2, "goodbye".to_string());
|
||||
/// ```
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
///
|
||||
/// To read values out of the refcell, you can use the `read` method which will retrun a `Ref`.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let map: Ref<_> = val.read();
|
||||
///
|
||||
/// let item = map.get(&1);
|
||||
@@ -43,7 +43,7 @@ use std::{
|
||||
///
|
||||
/// To get an &T out of the RefCell, you need to "reborrow" through the Ref:
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let read = val.read();
|
||||
/// let map = &*read;
|
||||
/// ```
|
||||
@@ -54,10 +54,10 @@ use std::{
|
||||
/// Typically this will be a collection like a HashMap or a Vec. To create new
|
||||
/// elements from the collection, we can use `read()` directly in our rsx!.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// rsx!{
|
||||
/// val.read().iter().map(|(k, v)| {
|
||||
/// rsx!{ key: "{k}", value: "{v}" }
|
||||
/// rsx!{ key: "{k}", "{v}" }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
@@ -66,9 +66,9 @@ use std::{
|
||||
/// "render" inside the iterator. For some cases you might need to collect into
|
||||
/// a temporary Vec.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let items = val.read().iter().map(|(k, v)| {
|
||||
/// cx.render(rsx!{ key: "{k}", value: "{v}" })
|
||||
/// cx.render(rsx!{ key: "{k}", "{v}" })
|
||||
/// });
|
||||
///
|
||||
/// // collect into a Vec
|
||||
@@ -80,9 +80,9 @@ use std::{
|
||||
///
|
||||
/// To access values from a `UseRef` in an async context, you need to detach it
|
||||
/// from the current scope's lifetime, making it a `'static` value. This is done
|
||||
/// by simply calling `ToOnwed` or `Clone`.
|
||||
/// by simply calling `to_owned` or `clone`.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// cx.spawn({
|
||||
@@ -95,15 +95,15 @@ use std::{
|
||||
/// ```
|
||||
///
|
||||
/// If you're working with lots of values like UseState and UseRef, you can use the
|
||||
/// `clone!` macro to make it easier to write the above code.
|
||||
/// `to_owned!` macro to make it easier to write the above code.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let val1 = use_ref(|| HashMap::<u32, String>::new());
|
||||
/// let val2 = use_ref(|| HashMap::<u32, String>::new());
|
||||
/// let val3 = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
/// cx.spawn({
|
||||
/// clone![val1, val2, val3];
|
||||
/// to_owned![val1, val2, val3];
|
||||
/// async move {
|
||||
/// some_work().await;
|
||||
/// val.write().insert(1, "hello".to_string());
|
||||
@@ -194,7 +194,7 @@ impl<T> UseRef<T> {
|
||||
/// Note: You can always "reborrow" the value through the RefCell.
|
||||
/// This method just does it for you automatically.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
///
|
||||
@@ -214,7 +214,7 @@ impl<T> UseRef<T> {
|
||||
/// Note: You can always "reborrow" the value through the RefCell.
|
||||
/// This method just does it for you automatically.
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// ```rust, no_run
|
||||
/// let val = use_ref(|| HashMap::<u32, String>::new());
|
||||
///
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -56,10 +56,9 @@ where
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let Ok(file_engine) =
|
||||
SerializedFileEngine::deserialize(deserializer) else{
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let file_engine = std::sync::Arc::new(file_engine);
|
||||
Ok(Some(file_engine))
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ impl Button {
|
||||
fn write_value(&self, rdom: &mut RealDom) {
|
||||
if let Some(mut text) = rdom.get_mut(self.text_id) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.value.clone();
|
||||
}
|
||||
}
|
||||
@@ -111,7 +113,9 @@ impl CustomElement for Button {
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
@@ -146,7 +150,9 @@ impl CustomElement for Button {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
@@ -155,7 +161,9 @@ impl CustomElement for Button {
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
|
||||
@@ -94,14 +94,18 @@ impl CheckBox {
|
||||
fn write_value(&self, mut root: NodeMut) {
|
||||
let single_char = {
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
Self::width(&el) == "1px" || Self::height(&el) == "1px"
|
||||
};
|
||||
let rdom = root.real_dom_mut();
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.text_id) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
let value = if single_char {
|
||||
if self.checked {
|
||||
"☑"
|
||||
@@ -156,7 +160,9 @@ impl CustomElement for CheckBox {
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
@@ -197,7 +203,9 @@ impl CustomElement for CheckBox {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_checked_attr(&el);
|
||||
@@ -207,7 +215,9 @@ impl CustomElement for CheckBox {
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ impl CustomElement for Input {
|
||||
}
|
||||
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
let input_type = el
|
||||
.attributes
|
||||
.get(&OwnedAttributeDiscription {
|
||||
|
||||
@@ -163,7 +163,9 @@ impl Slider {
|
||||
|
||||
if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) {
|
||||
let node_type = div.node_type_mut();
|
||||
let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut element) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
element.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
@@ -175,7 +177,9 @@ impl Slider {
|
||||
|
||||
if let Some(mut div) = rdom.get_mut(self.post_cursor_div) {
|
||||
let node_type = div.node_type_mut();
|
||||
let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut element) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
element.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
@@ -259,7 +263,9 @@ impl CustomElement for Slider {
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el.attributes.get(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
@@ -390,7 +396,9 @@ impl CustomElement for Slider {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_max_attr(&el);
|
||||
@@ -403,7 +411,9 @@ impl CustomElement for Slider {
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
|
||||
@@ -143,19 +143,25 @@ impl<C: TextLikeController> TextLike<C> {
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_before_first_cursor);
|
||||
}
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.highlighted_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_highlighted);
|
||||
}
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.post_cursor_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_after_second_cursor);
|
||||
}
|
||||
|
||||
@@ -288,7 +294,9 @@ impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
@@ -370,7 +378,9 @@ impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_max_width_attr(&el);
|
||||
@@ -381,7 +391,9 @@ impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user