codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ use wasm_bindgen::JsCast as _;
/// ```
pub struct Config {
pub(crate) hydrate: bool,
#[allow(dead_code)]
pub(crate) panic_hook: bool,
pub(crate) root: ConfigRoot,
#[cfg(feature = "document")]
+11 -9
View File
@@ -100,7 +100,7 @@ pub fn init_document() {
#[cfg(feature = "hydrate")]
pub fn init_fullstack_document() {
use dioxus_fullstack_hooks::{
use dioxus_fullstack_core::{
document::FullstackWebDocument, history::provide_fullstack_history_context,
};
@@ -131,14 +131,14 @@ impl Document for WebDocument {
/// Create a new meta tag in the head
fn create_meta(&self, props: MetaProps) {
queue_effect(move || {
append_element_to_head("meta", &props.attributes(), None);
_ = append_element_to_head("meta", &props.attributes(), None);
});
}
/// Create a new script tag in the head
fn create_script(&self, props: ScriptProps) {
queue_effect(move || {
append_element_to_head(
_ = append_element_to_head(
"script",
&props.attributes(),
props.script_contents().ok().as_deref(),
@@ -149,7 +149,7 @@ impl Document for WebDocument {
/// Create a new style tag in the head
fn create_style(&self, props: StyleProps) {
queue_effect(move || {
append_element_to_head(
_ = append_element_to_head(
"style",
&props.attributes(),
props.style_contents().ok().as_deref(),
@@ -160,7 +160,7 @@ impl Document for WebDocument {
/// Create a new link tag in the head
fn create_link(&self, props: LinkProps) {
queue_effect(move || {
append_element_to_head("link", &props.attributes(), None);
_ = append_element_to_head("link", &props.attributes(), None);
});
}
}
@@ -169,19 +169,21 @@ fn append_element_to_head(
local_name: &str,
attributes: &Vec<(&'static str, String)>,
text_content: Option<&str>,
) {
) -> Result<(), JsValue> {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let head = document.head().expect("document should have a head");
let element = document.create_element(local_name).unwrap();
let element = document.create_element(local_name)?;
for (name, value) in attributes {
element.set_attribute(name, value).unwrap();
element.set_attribute(name, value)?;
}
if text_content.is_some() {
element.set_text_content(text_content);
}
head.append_child(&element).unwrap();
head.append_child(&element)?;
Ok(())
}
/// Required to avoid blocking the Rust WASM thread.
+32 -26
View File
@@ -1,11 +1,13 @@
use crate::{WebFileData, WebFileEngine};
use super::{Synthetic, WebEventExt};
use dioxus_html::{
geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint},
input_data::{decode_mouse_button_set, MouseButton},
HasDragData, HasFileData, HasMouseData, InteractionElementOffset, InteractionLocation,
Modifiers, ModifiersInteraction, PointerInteraction,
FileData, HasDragData, HasFileData, HasMouseData, InteractionElementOffset,
InteractionLocation, Modifiers, ModifiersInteraction, PointerInteraction,
};
use web_sys::DragEvent;
use web_sys::{DragEvent, FileReader};
impl InteractionLocation for Synthetic<DragEvent> {
fn client_coordinates(&self) -> ClientPoint {
@@ -71,31 +73,35 @@ impl HasDragData for Synthetic<DragEvent> {
}
impl HasFileData for Synthetic<DragEvent> {
fn files(&self) -> Option<std::sync::Arc<dyn dioxus_html::FileEngine>> {
#[cfg(feature = "file_engine")]
{
use wasm_bindgen::JsCast;
let files = self
.event
.dyn_ref::<web_sys::DragEvent>()
.and_then(|drag_event| {
drag_event.data_transfer().and_then(|dt| {
dt.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f)
as std::sync::Arc<dyn dioxus_html::FileEngine>
})
})
})
});
fn files(&self) -> Vec<FileData> {
use wasm_bindgen::JsCast;
files
}
#[cfg(not(feature = "file_engine"))]
{
None
if let Some(target) = self.event.dyn_ref::<web_sys::DragEvent>() {
if let Some(data_transfer) = target.data_transfer() {
if let Some(file_list) = data_transfer.files() {
return WebFileEngine::new(file_list).to_files();
} else {
let items = data_transfer.items();
let mut files = vec![];
for i in 0..items.length() {
if let Some(item) = items.get(i) {
if item.kind() == "file" {
if let Ok(Some(file)) = item.get_as_file() {
let web_data =
WebFileData::new(file, FileReader::new().unwrap());
files.push(FileData::new(web_data));
}
}
}
}
return files;
}
}
} else {
tracing::warn!("DragEvent target was not a DragEvent");
}
vec![]
}
}
+38 -21
View File
@@ -1,30 +1,47 @@
use dioxus_html::HasFileData;
use dioxus_html::{FileData, HasFileData};
use web_sys::FileReader;
use crate::{WebFileData, WebFileEngine};
use super::Synthetic;
impl HasFileData for Synthetic<web_sys::Event> {
fn files(&self) -> Option<std::sync::Arc<dyn dioxus_html::FileEngine>> {
#[cfg(feature = "file_engine")]
{
use wasm_bindgen::JsCast;
fn files(&self) -> Vec<FileData> {
use wasm_bindgen::JsCast;
let target = self.event.target();
let files = self
.event
.dyn_ref()
.and_then(|input: &web_sys::HtmlInputElement| {
input.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
})
})
});
files
}
#[cfg(not(feature = "file_engine"))]
if let Some(target) = target
.clone()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
{
None
if let Some(file_list) = target.files() {
return WebFileEngine::new(file_list).to_files();
}
}
if let Some(target) = target.and_then(|t| t.dyn_into::<web_sys::DragEvent>().ok()) {
if let Some(data_transfer) = target.data_transfer() {
if let Some(file_list) = data_transfer.files() {
return WebFileEngine::new(file_list).to_files();
} else {
let items = data_transfer.items();
let mut files = vec![];
for i in 0..items.length() {
if let Some(item) = items.get(i) {
if item.kind() == "file" {
if let Ok(Some(file)) = item.get_as_file() {
let web_data =
WebFileData::new(file, FileReader::new().unwrap());
files.push(FileData::new(web_data));
}
}
}
}
return files;
}
}
}
vec![]
}
}
+45 -57
View File
@@ -1,6 +1,6 @@
use std::{any::Any, collections::HashMap};
use dioxus_html::{FormValue, HasFileData, HasFormData};
use dioxus_html::{FileData, FormValue, HasFileData, HasFormData};
use js_sys::Array;
use wasm_bindgen::JsValue;
use wasm_bindgen::{prelude::wasm_bindgen, JsCast};
@@ -10,7 +10,7 @@ use super::WebEventExt;
pub(crate) struct WebFormData {
element: Element,
raw: Event,
event: Event,
}
impl WebEventExt for dioxus_html::FormData {
@@ -23,8 +23,8 @@ impl WebEventExt for dioxus_html::FormData {
}
impl WebFormData {
pub fn new(element: Element, raw: Event) -> Self {
Self { element, raw }
pub fn new(element: Element, event: Event) -> Self {
Self { element, event }
}
}
@@ -32,39 +32,39 @@ impl HasFormData for WebFormData {
fn value(&self) -> String {
let target = &self.element;
target
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
},
_ => {
input.value()
}
}
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener")
}
fn values(&self) -> HashMap<String, FormValue> {
@@ -101,32 +101,20 @@ impl HasFormData for WebFormData {
}
fn as_any(&self) -> &dyn Any {
&self.raw as &dyn Any
&self.event as &dyn Any
}
}
impl HasFileData for WebFormData {
fn files(&self) -> Option<std::sync::Arc<dyn dioxus_html::FileEngine>> {
#[cfg(feature = "file_engine")]
{
let files = self
.element
.dyn_ref()
.and_then(|input: &web_sys::HtmlInputElement| {
input.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
})
})
});
files
}
#[cfg(not(feature = "file_engine"))]
{
None
}
fn files(&self) -> Vec<FileData> {
use wasm_bindgen::JsCast;
self.event
.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
.and_then(|input| input.files())
.map(crate::files::WebFileEngine::new)
.map(|engine| engine.to_files())
.unwrap_or_default()
}
}
-133
View File
@@ -1,133 +0,0 @@
use std::any::Any;
use dioxus_html::FileEngine;
use futures_channel::oneshot;
use js_sys::Uint8Array;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{File, FileList, FileReader};
/// A file engine for the web platform
pub struct WebFileEngine {
file_reader: FileReader,
file_list: FileList,
}
impl WebFileEngine {
/// Create a new file engine from a file list
pub fn new(file_list: FileList) -> Option<Self> {
Some(Self {
file_list,
file_reader: FileReader::new().ok()?,
})
}
fn len(&self) -> usize {
self.file_list.length() as usize
}
fn get(&self, index: usize) -> Option<File> {
self.file_list.item(index as u32)
}
fn find(&self, name: &str) -> Option<File> {
(0..self.len())
.filter_map(|i| self.get(i))
.find(|f| f.name() == name)
}
}
#[async_trait::async_trait(?Send)]
impl FileEngine for WebFileEngine {
fn files(&self) -> Vec<String> {
(0..self.len())
.filter_map(|i| self.get(i).map(|f| f.name()))
.collect()
}
async fn file_size(&self, file: &str) -> Option<u64> {
let file = self.find(file)?;
Some(file.size() as u64)
}
// read a file to bytes
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_array_buffer(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
let as_u8_arr = Uint8Array::new(&js_val);
let as_u8_vec = as_u8_arr.to_vec();
Some(as_u8_vec)
} else {
None
}
}
// read a file to string
async fn read_file_to_string(&self, file: &str) -> Option<String> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_text(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
js_val.as_string()
} else {
None
}
}
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
let file = self.find(file)?;
Some(Box::new(file))
}
}
/// Helper trait for WebFileEngine
#[async_trait::async_trait(?Send)]
pub trait WebFileEngineExt {
/// returns web_sys::File
async fn get_web_file(&self, file: &str) -> Option<web_sys::File>;
}
#[async_trait::async_trait(?Send)]
impl WebFileEngineExt for std::sync::Arc<dyn FileEngine> {
async fn get_web_file(&self, file: &str) -> Option<web_sys::File> {
let native_file = self.get_native_file(file).await?;
let ret = native_file.downcast::<web_sys::File>().ok()?;
Some(*ret)
}
}
+189
View File
@@ -0,0 +1,189 @@
use dioxus_core::AnyhowContext;
use dioxus_html::{bytes::Bytes, FileData, NativeFileData};
use futures_channel::oneshot;
use js_sys::Uint8Array;
use send_wrapper::SendWrapper;
use std::{pin::Pin, prelude::rust_2024::Future};
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{File, FileList, FileReader};
/// A file representation for the web platform
#[derive(Clone)]
pub struct WebFileData {
file: File,
reader: FileReader,
}
unsafe impl Send for WebFileData {}
unsafe impl Sync for WebFileData {}
impl WebFileData {
/// Create a new WebFileData from a web_sys::File
pub fn new(file: File, reader: FileReader) -> Self {
Self { file, reader }
}
}
impl NativeFileData for WebFileData {
fn name(&self) -> String {
self.file.name()
}
fn size(&self) -> u64 {
self.file.size() as u64
}
fn last_modified(&self) -> u64 {
self.file.last_modified() as u64
}
fn read_bytes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::Error>> + 'static>> {
let file_reader = self.reader.clone();
let file_reader_ = self.reader.clone();
let file = self.file.clone();
Box::pin(async move {
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
file_reader_.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
file_reader_
.read_as_array_buffer(&file)
.ok()
.context("Failed to read file")?;
let js_val = tx.await?.ok().context("Failed to read file")?;
let as_u8_arr = Uint8Array::new(&js_val);
let as_u8_vec = as_u8_arr.to_vec().into();
Ok(as_u8_vec)
})
}
fn read_string(
&self,
) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::Error>> + 'static>> {
let file_reader = self.reader.clone();
let file_reader_ = self.reader.clone();
let file = self.file.clone();
Box::pin(async move {
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
file_reader_.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
file_reader_
.read_as_text(&file)
.ok()
.context("Failed to read file")?;
let js_val = tx.await?.ok().context("Failed to read file")?;
let as_string = js_val.as_string().context("Failed to read file")?;
Ok(as_string)
})
}
/// we'd like to use `blob` to readable stream here, but we cannot.
///
/// We just read the entire file into memory and return it as a single chunk.
/// This is not super great, especially given the wasm <-> js boundary duplication cost.
///
/// For more efficient streaming of byte data, consider using the dedicated FileStream type which
/// goes directly from `File` to fetch request body without going through Rust.
///
/// We should maybe update these APIs to use our own custom `ByteBuffer` type to avoid going through `Vec<u8>`?
fn byte_stream(
&self,
) -> Pin<Box<dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::Error>> + 'static + Send>>
{
let file = self.file.dyn_ref::<web_sys::Blob>().unwrap().clone();
Box::pin(SendWrapper::new(futures_util::stream::once(async move {
let array_buff = wasm_bindgen_futures::JsFuture::from(file.array_buffer())
.await
.unwrap();
let as_uint_array = array_buff.dyn_into::<Uint8Array>().unwrap();
Ok(as_uint_array.to_vec().into())
})))
}
fn inner(&self) -> &dyn std::any::Any {
&self.file
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(self.file.name())
}
fn content_type(&self) -> Option<String> {
let type_ = self.file.type_();
if type_.is_empty() {
None
} else {
Some(type_)
}
}
}
/// A file engine for the web platform
#[derive(Clone)]
pub(crate) struct WebFileEngine {
file_list: FileList,
}
impl WebFileEngine {
/// Create a new file engine from a file list
pub fn new(file_list: FileList) -> Self {
Self { file_list }
}
fn len(&self) -> usize {
self.file_list.length() as usize
}
fn get(&self, index: usize) -> Option<File> {
self.file_list.item(index as u32)
}
pub fn to_files(&self) -> Vec<FileData> {
(0..self.len())
.filter_map(|i| self.get(i))
.map(|file| {
FileData::new(WebFileData {
file,
reader: FileReader::new().unwrap(),
})
})
.collect()
}
}
/// Helper trait for extracting the underlying `web_sys::File` from a `FileData`
pub trait WebFileExt {
/// returns web_sys::File
fn get_web_file(&self) -> Option<web_sys::File>;
}
impl WebFileExt for FileData {
fn get_web_file(&self) -> Option<web_sys::File> {
self.inner().downcast_ref::<web_sys::File>().cloned()
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use dioxus_core::{
AttributeValue, DynamicNode, ElementId, ScopeId, ScopeState, SuspenseBoundaryProps,
SuspenseContext, TemplateNode, VNode, VirtualDom,
};
use dioxus_fullstack_protocol::HydrationContext;
use dioxus_fullstack_core::HydrationContext;
use futures_channel::mpsc::UnboundedReceiver;
use std::fmt::Write;
use RehydrationError::*;
+6 -9
View File
@@ -4,8 +4,6 @@
//! # Dioxus Web
use std::time::Duration;
pub use crate::cfg::Config;
use crate::hydration::SuspenseMessage;
use dioxus_core::VirtualDom;
@@ -22,17 +20,16 @@ pub use events::*;
#[cfg(feature = "document")]
mod document;
#[cfg(feature = "file_engine")]
mod file_engine;
#[cfg(feature = "document")]
mod history;
#[cfg(feature = "document")]
pub use document::WebDocument;
#[cfg(feature = "file_engine")]
pub use file_engine::*;
#[cfg(feature = "document")]
pub use history::{HashHistory, WebHistory};
mod files;
pub use files::*;
#[cfg(all(feature = "devtools", debug_assertions))]
mod devtools;
@@ -95,7 +92,7 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
#[cfg(feature = "hydrate")]
{
use dioxus_fullstack_protocol::HydrationContext;
use dioxus_fullstack_core::HydrationContext;
websys_dom.skip_mutations = true;
// Get the initial hydration data from the client
@@ -139,7 +136,7 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
// Provide a hydration compatible create error boundary method
dioxus_core::ScopeId::ROOT.in_runtime(|| {
dioxus_core::provide_create_error_boundary(
dioxus_fullstack_hooks::errors::init_error_boundary,
dioxus_fullstack_core::init_error_boundary,
);
});
#[cfg(feature = "document")]
@@ -235,7 +232,7 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
"Hot-patch success!",
&format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
devtools::ToastLevel::Success,
Duration::from_millis(2000),
std::time::Duration::from_millis(2000),
false,
);
}