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

@@ -4,10 +4,10 @@ const { test, expect } = require("@playwright/test");
test("optimized scripts run", async ({ page }) => {
await page.goto("http://localhost:8989");
// Expect the page to load the script after optimizations have been applied. The script
// should add an editor to the page that shows a main function
const main = page.locator("#main");
await expect(main).toContainText("hi");
// // Expect the page to load the script after optimizations have been applied. The script
// // should add an editor to the page that shows a main function
// const main = page.locator("#main");
// await expect(main).toContainText("hi");
// Expect the page to contain an image with the id "some_image"
const image = page.locator("#some_image");
@@ -11,7 +11,7 @@ dioxus = { workspace = true, features = ["web"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
[build-dependencies]
reqwest = { workspace = true, features = ["blocking"] }
flate2 = "1.1.2"
tar = "0.4.44"
# [build-dependencies]
# reqwest = { workspace = true, features = ["blocking"] }
# flate2 = "1.1.2"
# tar = "0.4.44"
@@ -1,16 +1,16 @@
use std::path::PathBuf;
fn main() {
// If the monaco editor folder doesn't exist, download it
let monaco_path = PathBuf::from("monaco-editor");
if monaco_path.exists() {
return;
}
// use std::path::PathBuf;
let url = "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz";
let bytes = reqwest::blocking::get(url).unwrap().bytes().unwrap();
let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(bytes.as_ref()));
let monaco_path_partial = PathBuf::from("partial-monaco-editor");
archive.unpack(&monaco_path_partial).unwrap();
std::fs::rename(monaco_path_partial, monaco_path).unwrap();
// // If the monaco editor folder doesn't exist, download it
// let monaco_path = PathBuf::from("monaco-editor");
// if monaco_path.exists() {
// return;
// }
// let url = "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz";
// let bytes = reqwest::blocking::get(url).unwrap().bytes().unwrap();
// let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(bytes.as_ref()));
// let monaco_path_partial = PathBuf::from("partial-monaco-editor");
// archive.unpack(&monaco_path_partial).unwrap();
// std::fs::rename(monaco_path_partial, monaco_path).unwrap();
}
@@ -2,7 +2,6 @@
use dioxus::prelude::*;
const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs");
const SOME_IMAGE: Asset = asset!("/images/toasts.png", AssetOptions::image().with_avif());
const SOME_IMAGE_WITH_THE_SAME_URL: Asset =
asset!("/images/toasts.png", AssetOptions::image().with_jpg());
@@ -24,26 +23,29 @@ fn main() {
#[component]
fn App() -> Element {
let script = format!("(() => {{
require.config({{ paths: {{ vs: '{MONACO_FOLDER}' }} }});
// todo: test monaco more....
// const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs");
// let script = format!("(() => {{
// require.config({{ paths: {{ vs: '{MONACO_FOLDER}' }} }});
// require(['vs/editor/editor.main'], () => {{
// var model = monaco.editor.createModel('fn main() {{\\n\\tprintln!(\\\"hi\\\")\\n}}', 'rust');
// var editor = monaco.editor.create(document.getElementById('editor'));
// editor.setModel(model);
// }})
// }})()");
require(['vs/editor/editor.main'], () => {{
var model = monaco.editor.createModel('fn main() {{\\n\\tprintln!(\\\"hi\\\")\\n}}', 'rust');
var editor = monaco.editor.create(document.getElementById('editor'));
editor.setModel(model);
}})
}})()");
rsx! {
div {
id: "editor",
width: "100vw",
height: "100vw",
}
// Monaco script
script {
src: "{MONACO_FOLDER}/loader.js",
"onload": script
}
// // Monaco script
// script {
// src: "{MONACO_FOLDER}/loader.js",
// "onload": script
// }
img {
id: "some_image",
src: "{SOME_IMAGE}"
@@ -4,6 +4,10 @@ const { test, expect } = require("@playwright/test");
test("errors", async ({ page }) => {
await page.goto("http://localhost:3232");
// give the page time to finish loading resources
await page.waitForLoadState("networkidle");
await page.waitForTimeout(2000);
// Make sure the error that was thrown on the server is shown in the error boundary on the client
const errors = page.locator("#error-fallback-button");
await expect(errors).toContainText("Error fallback button clicked 0 times");
@@ -6,7 +6,7 @@
use dioxus::prelude::*;
fn main() {
launch(app);
dioxus::launch(app);
}
fn app() -> Element {
@@ -5,12 +5,12 @@
// - 500 Routes
#![allow(non_snake_case)]
use dioxus::{prelude::*, CapturedError};
use dioxus::prelude::*;
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
})
.launch(app);
}
@@ -50,9 +50,7 @@ fn Blog(id: i32) -> Element {
#[component]
fn ThrowsError() -> Element {
return Err(RenderError::Aborted(CapturedError::from_display(
"This route tests uncaught errors in the server",
)));
dioxus::core::bail!("This route tests uncaught errors in the server",)
}
#[component]
+1 -1
View File
@@ -95,7 +95,7 @@ test("assets cache correctly", async ({ page }) => {
// Wait for the asset image to be loaded
const assetImageResponse = await assetImageFuture;
console.log("Asset Image Response:", assetImageResponse);
// console.log("Asset Image Response:", assetImageResponse);
// Make sure the asset image cache control header does not contain immutable
const assetCacheControl = assetImageResponse.headers()["cache-control"];
console.log("Cache-Control header:", assetCacheControl);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+54 -37
View File
@@ -5,14 +5,13 @@
// - Hydration
#![allow(non_snake_case)]
use dioxus::fullstack::{codec::JsonEncoding, commit_initial_chunk, BoxedStream, Websocket};
use dioxus::prelude::*;
use futures::{channel::mpsc, SinkExt, StreamExt};
use dioxus::fullstack::{commit_initial_chunk, Websocket};
use dioxus::{fullstack::WebSocketOptions, prelude::*};
fn main() {
dioxus::LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
})
.with_context(1234u32)
.launch(app);
@@ -23,7 +22,7 @@ fn app() -> Element {
let mut text = use_signal(|| "...".to_string());
rsx! {
document::Title { "hello axum! {count}" }
Title { "hello axum! {count}" }
h1 { "hello axum! {count}" }
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
button {
@@ -75,12 +74,14 @@ fn DefaultServerFnCodec() -> Element {
#[cfg(feature = "server")]
async fn assert_server_context_provided() {
use dioxus::server::{extract, FromContext};
let FromContext(i): FromContext<u32> = extract().await.unwrap();
assert_eq!(i, 1234u32);
// todo!("replace server context....")
// use dioxus::server::{extract, FromContext};
// let FromContext(i): FromContext<u32> = extract().await.unwrap();
// assert_eq!(i, 1234u32);
}
#[server(PostServerData)]
// #[server(PostServerData)]
#[server]
async fn post_server_data(data: String) -> ServerFnResult {
assert_server_context_provided().await;
println!("Server received: {}", data);
@@ -88,7 +89,8 @@ async fn post_server_data(data: String) -> ServerFnResult {
Ok(())
}
#[server(GetServerData)]
// #[server(GetServerData)]
#[server]
async fn get_server_data() -> ServerFnResult<String> {
assert_server_context_provided().await;
Ok("Hello from the server!".to_string())
@@ -105,9 +107,10 @@ async fn get_server_data_empty_vec(empty_vec: Vec<String>) -> ServerFnResult<Vec
#[server]
async fn server_error() -> ServerFnResult<String> {
use dioxus_core::AnyhowContext;
assert_server_context_provided().await;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
Err(ServerFnError::new("the server threw an error!"))
Err(None.context("Server error occurred")?)
}
#[component]
@@ -165,49 +168,50 @@ fn DocumentElements() -> Element {
fn Assets() -> Element {
#[used]
static _ASSET: Asset = asset!("/assets/image.png");
#[used]
static _OTHER_ASSET: Asset = asset!("/assets/nested");
static _STATIC_NO_HASH: Asset = asset!(
"/assets/image.png",
AssetOptions::image().with_hash_suffix(false)
);
#[used]
static _UNHASHED_FOLDER: Asset = asset!(
"/assets/nested/",
AssetOptions::folder().with_hash_suffix(false)
);
#[used]
static _EMBEDDED_FOLDER: Asset = asset!("/assets/nested");
rsx! {
img {
src: asset!("/assets/image.png"),
}
img {
src: "/assets/image.png",
src: "{_EMBEDDED_FOLDER}/image.png",
}
img {
src: "/assets/nested/image.png",
src: "{_UNHASHED_FOLDER}/image.png",
}
img {
src: "/assets/image.png",
}
}
}
#[server(protocol = Websocket<JsonEncoding, JsonEncoding>)]
async fn echo_ws(
input: BoxedStream<String, ServerFnError>,
) -> ServerFnResult<BoxedStream<String, ServerFnError>> {
let mut input = input;
let (mut tx, rx) = mpsc::channel(1);
tokio::spawn(async move {
while let Some(msg) = input.next().await {
let _ = tx.send(msg.map(|msg| msg.to_ascii_uppercase())).await;
}
});
Ok(rx.into())
}
/// This component tests websocket server functions
#[component]
fn WebSockets() -> Element {
let mut received = use_signal(String::new);
use_future(move || async move {
let (mut tx, rx) = mpsc::channel(1);
let mut receiver = echo_ws(rx.into()).await.unwrap();
tx.send(Ok("hello world".to_string())).await.unwrap();
while let Some(Ok(msg)) = receiver.next().await {
println!("Received: {}", msg);
received.set(msg);
let socket = echo_ws(WebSocketOptions::default()).await.unwrap();
socket.send("hello world".to_string()).await.unwrap();
while let Ok(msg) = socket.recv().await {
received.write().push_str(&msg);
}
});
@@ -218,3 +222,16 @@ fn WebSockets() -> Element {
}
}
}
#[get("/api/echo_ws")]
async fn echo_ws(options: WebSocketOptions) -> Result<Websocket> {
info!("Upgrading to websocket");
Ok(options.on_upgrade(
|mut tx: dioxus::fullstack::TypedWebsocket<String, String>| async move {
while let Ok(msg) = tx.recv().await {
let _ = tx.send(msg.to_ascii_uppercase()).await;
}
},
))
}
+14 -44
View File
@@ -42,36 +42,6 @@ module.exports = defineConfig({
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
@@ -87,7 +57,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "web"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 9990',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 9990',
port: 9990,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -96,7 +66,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "web-routing"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 2020',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2020',
port: 2020,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -105,7 +75,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "web-hash-routing"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 2021',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2021',
port: 2021,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -114,7 +84,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "fullstack"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 3333',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3333',
port: 3333,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -123,7 +93,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "fullstack-errors"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 3232',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3232',
port: 3232,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -132,7 +102,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "fullstack-mounted"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7777',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7777',
port: 7777,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -141,7 +111,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "fullstack-routing"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 8888',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 8888',
port: 8888,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -150,7 +120,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "suspense-carousel"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 4040',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 4040',
port: 4040,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -159,7 +129,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "nested-suspense"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 5050',
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 5050',
port: 5050,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -168,7 +138,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "nested-suspense"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --bin nested-suspense-ssg --force-sequential --web --ssg --addr "127.0.0.1" --port 6060',
'cargo run --package dioxus-cli --release -- run --bin nested-suspense-ssg --force-sequential --web --ssg --addr "127.0.0.1" --port 6060',
port: 6060,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -178,7 +148,7 @@ module.exports = defineConfig({
cwd: path.join(process.cwd(), "cli-optimization"),
// Remove the cache folder for the cli-optimization build to force a full cache reset
command:
'cargo run --package dioxus-cli --release -- run --verbose --addr "127.0.0.1" --port 8989',
'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 8989',
port: 8989,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -187,7 +157,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "wasm-split-harness"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --bin wasm-split-harness --web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
'cargo run --package dioxus-cli --release -- run --bin wasm-split-harness --web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
port: 8001,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -196,7 +166,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "default-features-disabled"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --addr "127.0.0.1" --port 8002',
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8002',
port: 8002,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -205,7 +175,7 @@ module.exports = defineConfig({
{
cwd: path.join(process.cwd(), "barebones-template"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --addr "127.0.0.1" --port 8123',
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8123',
port: 8123,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
@@ -111,7 +111,7 @@ fn NestedSuspendedComponent(id: i32) -> Element {
fn main() {
LaunchBuilder::new()
.with_cfg(server_only! {
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
dioxus::server::ServeConfig::builder().enable_out_of_order_streaming()
})
.launch(app);
}