Fix ssg race condition (#3521)
* Fix server crash logging and opening fullstack apps * Fix race condition during SSG * move route prerendering into a separate stage * Fix static site generation with dx serve * only proxy the fullstack server if ssg is disabled * test nested-suspense with ssg in playwright
This commit is contained in:
1 parent
343c48f5ad
commit
ecc419df0a
18 files changed
+370
-214
No files matched your search
@@ -0,0 +1,50 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
test("nested suspense resolves", async ({ page }) => {
|
||||
// Wait for the dev server to reload
|
||||
await page.goto("http://localhost:5050");
|
||||
// Then wait for the page to start loading
|
||||
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
||||
|
||||
// Expect the page to contain the suspense result from the server
|
||||
const mainMessageTitle = page.locator("#title-0");
|
||||
await expect(mainMessageTitle).toContainText("The robot says hello world");
|
||||
const mainMessageBody = page.locator("#body-0");
|
||||
await expect(mainMessageBody).toContainText(
|
||||
"The robot becomes sentient and says hello world"
|
||||
);
|
||||
|
||||
// And expect the title to have resolved on the client
|
||||
await expect(page).toHaveTitle("The robot says hello world");
|
||||
|
||||
// Nested suspense should be resolved
|
||||
const nestedMessageTitle1 = page.locator("#title-1");
|
||||
await expect(nestedMessageTitle1).toContainText("The world says hello back");
|
||||
const nestedMessageBody1 = page.locator("#body-1");
|
||||
await expect(nestedMessageBody1).toContainText(
|
||||
"In a stunning turn of events, the world collectively unites and says hello back"
|
||||
);
|
||||
|
||||
const nestedMessageDiv2 = page.locator("#children-2");
|
||||
await expect(nestedMessageDiv2).toBeEmpty();
|
||||
const nestedMessageTitle2 = page.locator("#title-2");
|
||||
await expect(nestedMessageTitle2).toContainText("Goodbye Robot");
|
||||
const nestedMessageBody2 = page.locator("#body-2");
|
||||
await expect(nestedMessageBody2).toContainText("The robot says goodbye");
|
||||
|
||||
const nestedMessageDiv3 = page.locator("#children-3");
|
||||
await expect(nestedMessageDiv3).toBeEmpty();
|
||||
const nestedMessageTitle3 = page.locator("#title-3");
|
||||
await expect(nestedMessageTitle3).toContainText("Goodbye World");
|
||||
const nestedMessageBody3 = page.locator("#body-3");
|
||||
await expect(nestedMessageBody3).toContainText("The world says goodbye");
|
||||
|
||||
// Deeply nested suspense should be resolved
|
||||
const nestedMessageDiv4 = page.locator("#children-4");
|
||||
await expect(nestedMessageDiv4).toBeEmpty();
|
||||
const nestedMessageTitle4 = page.locator("#title-4");
|
||||
await expect(nestedMessageTitle4).toContainText("Hello World");
|
||||
const nestedMessageBody4 = page.locator("#body-4");
|
||||
await expect(nestedMessageBody4).toContainText("The world says hello again");
|
||||
});
|
||||
@@ -15,3 +15,12 @@ tokio = { workspace = true, features = ["full"], optional = true }
|
||||
default = []
|
||||
server = ["dioxus/server", "tokio"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
# We need a separate bin for the SSG build to avoid conflicting server caches
|
||||
[[bin]]
|
||||
name = "nested-suspense-ssg"
|
||||
path = "src/ssg.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nested-suspense"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,114 @@
|
||||
// This test is used by playwright configured in the root of the repo
|
||||
// Tests:
|
||||
// - SEO without JS
|
||||
// - Streaming hydration
|
||||
// - Suspense
|
||||
// - Server functions
|
||||
//
|
||||
// Without Javascript, content may not load into the right location, but it should still be somewhere in the html even if it is invisible
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn app() -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: move |_| rsx! {},
|
||||
document::Style {
|
||||
href: asset!("/assets/style.css")
|
||||
}
|
||||
LoadTitle {}
|
||||
}
|
||||
MessageWithLoader { id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn MessageWithLoader(id: usize) -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: move |_| rsx! {
|
||||
"Loading {id}..."
|
||||
},
|
||||
Message { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoadTitle() -> Element {
|
||||
let title = use_server_future(move || server_content(0))?()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
rsx! {
|
||||
"title loaded"
|
||||
document::Title { "{title.title}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Message(id: usize) -> Element {
|
||||
let message = use_server_future(move || server_content(id))?()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
rsx! {
|
||||
h2 {
|
||||
id: "title-{id}",
|
||||
"{message.title}"
|
||||
}
|
||||
p {
|
||||
id: "body-{id}",
|
||||
"{message.body}"
|
||||
}
|
||||
div {
|
||||
id: "children-{id}",
|
||||
padding: "10px",
|
||||
for child in message.children {
|
||||
MessageWithLoader { id: child }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Content {
|
||||
title: String,
|
||||
body: String,
|
||||
children: Vec<usize>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn server_content(id: usize) -> Result<Content, ServerFnError> {
|
||||
let content_tree = [
|
||||
Content {
|
||||
title: "The robot says hello world".to_string(),
|
||||
body: "The robot becomes sentient and says hello world".to_string(),
|
||||
children: vec![1, 2, 3],
|
||||
},
|
||||
Content {
|
||||
title: "The world says hello back".to_string(),
|
||||
body: "In a stunning turn of events, the world collectively unites and says hello back"
|
||||
.to_string(),
|
||||
children: vec![4],
|
||||
},
|
||||
Content {
|
||||
title: "Goodbye Robot".to_string(),
|
||||
body: "The robot says goodbye".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
Content {
|
||||
title: "Goodbye World".to_string(),
|
||||
body: "The world says goodbye".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
Content {
|
||||
title: "Hello World".to_string(),
|
||||
body: "The world says hello again".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
];
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
Ok(content_tree[id].clone())
|
||||
}
|
||||
@@ -1,123 +1,12 @@
|
||||
// This test is used by playwright configured in the root of the repo
|
||||
// Tests:
|
||||
// - SEO without JS
|
||||
// - Streaming hydration
|
||||
// - Suspense
|
||||
// - Server functions
|
||||
//
|
||||
// Without Javascript, content may not load into the right location, but it should still be somewhere in the html even if it is invisible
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use nested_suspense::app;
|
||||
|
||||
fn main() {
|
||||
LaunchBuilder::new()
|
||||
.with_cfg(server_only! {
|
||||
dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
|
||||
ServeConfig::builder()
|
||||
.enable_out_of_order_streaming()
|
||||
})
|
||||
.launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: move |_| rsx! {},
|
||||
document::Style {
|
||||
href: asset!("/assets/style.css")
|
||||
}
|
||||
LoadTitle {}
|
||||
}
|
||||
MessageWithLoader { id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn MessageWithLoader(id: usize) -> Element {
|
||||
rsx! {
|
||||
SuspenseBoundary {
|
||||
fallback: move |_| rsx! {
|
||||
"Loading {id}..."
|
||||
},
|
||||
Message { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoadTitle() -> Element {
|
||||
let title = use_server_future(move || server_content(0))?()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
rsx! {
|
||||
"title loaded"
|
||||
document::Title { "{title.title}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Message(id: usize) -> Element {
|
||||
let message = use_server_future(move || server_content(id))?()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
rsx! {
|
||||
h2 {
|
||||
id: "title-{id}",
|
||||
"{message.title}"
|
||||
}
|
||||
p {
|
||||
id: "body-{id}",
|
||||
"{message.body}"
|
||||
}
|
||||
div {
|
||||
id: "children-{id}",
|
||||
padding: "10px",
|
||||
for child in message.children {
|
||||
MessageWithLoader { id: child }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Content {
|
||||
title: String,
|
||||
body: String,
|
||||
children: Vec<usize>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn server_content(id: usize) -> Result<Content, ServerFnError> {
|
||||
let content_tree = [
|
||||
Content {
|
||||
title: "The robot says hello world".to_string(),
|
||||
body: "The robot becomes sentient and says hello world".to_string(),
|
||||
children: vec![1, 2, 3],
|
||||
},
|
||||
Content {
|
||||
title: "The world says hello back".to_string(),
|
||||
body: "In a stunning turn of events, the world collectively unites and says hello back"
|
||||
.to_string(),
|
||||
children: vec![4],
|
||||
},
|
||||
Content {
|
||||
title: "Goodbye Robot".to_string(),
|
||||
body: "The robot says goodbye".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
Content {
|
||||
title: "Goodbye World".to_string(),
|
||||
body: "The world says goodbye".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
Content {
|
||||
title: "Hello World".to_string(),
|
||||
body: "The world says hello again".to_string(),
|
||||
children: vec![],
|
||||
},
|
||||
];
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
Ok(content_tree[id].clone())
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
use nested_suspense::app;
|
||||
|
||||
fn main() {
|
||||
dioxus::LaunchBuilder::new()
|
||||
.with_cfg(server_only! {
|
||||
ServeConfig::builder()
|
||||
.incremental(
|
||||
IncrementalRendererConfig::new()
|
||||
.static_dir(
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("public")
|
||||
)
|
||||
)
|
||||
.enable_out_of_order_streaming()
|
||||
})
|
||||
.launch(app);
|
||||
}
|
||||
|
||||
#[server(endpoint = "static_routes")]
|
||||
async fn static_routes() -> Result<Vec<String>, ServerFnError> {
|
||||
Ok(vec!["/".to_string()])
|
||||
}
|
||||
@@ -129,5 +129,14 @@ module.exports = defineConfig({
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "nested-suspense"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- serve --bin nested-suspense-ssg --force-sequential --platform web --ssg --addr "127.0.0.1" --port 6060',
|
||||
port: 6060,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in new issue
Block a user