mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
feat: support SSR
This commit is contained in:
parent
68659411b9
commit
66791d88f2
13 changed files with 74 additions and 326 deletions
|
@ -23,7 +23,7 @@ pub fn SwitchVersion() -> impl IntoView {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
let version = RwSignal::new(None::<String>);
|
||||
// let version = RwSignal::new(None::<String>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,28 +9,23 @@ crate-type = ["cdylib", "rlib"]
|
|||
[dependencies]
|
||||
axum = { version = "0.7.4", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.6.10" }
|
||||
leptos_axum = { version = "0.6.10", optional = true }
|
||||
leptos_meta = { version = "0.6.10" }
|
||||
leptos_router = { version = "0.6.10" }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.35.1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.1", features = ["fs"], optional = true }
|
||||
leptos = { version = "0.7.0-beta" }
|
||||
leptos_axum = { version = "0.7.0-beta", optional = true }
|
||||
leptos_meta = { version = "0.7.0-beta" }
|
||||
leptos_router = { version = "0.7.0-beta" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "=0.2.92"
|
||||
thiserror = "1.0.56"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
http = "0.2.8"
|
||||
thiserror = "1"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
http = "1"
|
||||
|
||||
demo = { path = "../../demo", default-features = false }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"leptos_meta/hydrate",
|
||||
"leptos_router/hydrate",
|
||||
"demo/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
|
|
74
examples/ssr_axum/end2end/package-lock.json
generated
74
examples/ssr_axum/end2end/package-lock.json
generated
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
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: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,9 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Welcome to Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
|
||||
});
|
21
examples/ssr_axum/src/app.rs
Normal file
21
examples/ssr_axum/src/app.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
pub use demo::App;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::MetaTags;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use demo::App;
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! {<App/>});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
pub mod fileserv;
|
||||
pub mod app;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use demo::App;
|
||||
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
use crate::app::*;
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,28 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::post, Router};
|
||||
use demo::App;
|
||||
use leptos::*;
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_axum::fileserv::file_and_error_handler;
|
||||
use ssr_axum::app::*;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||
// For deployment these variables are:
|
||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||
// The file would need to be included with the executable when moved to deployment
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log::info!("listening on http://{}", &addr);
|
||||
log!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#[cfg(not(feature = "ssr"))]
|
||||
use leptos::prelude::RenderEffect;
|
||||
use leptos::{
|
||||
prelude::{MaybeProp, Memo, Oco, RwSignal},
|
||||
prelude::{MaybeProp, Memo, Oco, RenderEffect, RwSignal},
|
||||
reactive_graph::traits::{Get, Update, With, WithUntracked},
|
||||
tachys::renderer::DomRenderer,
|
||||
};
|
||||
|
@ -10,8 +8,11 @@ use std::{collections::HashSet, sync::Arc};
|
|||
#[derive(Clone, Default)]
|
||||
pub struct ClassList {
|
||||
value: RwSignal<HashSet<Oco<'static, str>>>,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
effects_oco: Vec<Arc<RenderEffect<Oco<'static, str>>>>,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
effects_option_oco: Vec<Arc<RenderEffect<Option<Oco<'static, str>>>>>,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
effects_bool: Vec<Arc<RenderEffect<bool>>>,
|
||||
}
|
||||
|
||||
|
@ -33,7 +34,7 @@ impl ClassList {
|
|||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let name = f();
|
||||
self.0.update(|set| {
|
||||
self.value.update(|set| {
|
||||
set.insert(name);
|
||||
});
|
||||
}
|
||||
|
@ -62,7 +63,7 @@ impl ClassList {
|
|||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
if let Some(name) = f() {
|
||||
self.0.update(|set| {
|
||||
self.value.update(|set| {
|
||||
set.insert(name);
|
||||
});
|
||||
}
|
||||
|
@ -103,11 +104,11 @@ impl ClassList {
|
|||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let new = f();
|
||||
self.0.update(|set| {
|
||||
if new {
|
||||
if new {
|
||||
self.value.update(|set| {
|
||||
set.insert(name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
|
|
|
@ -3,11 +3,14 @@ use cfg_if::cfg_if;
|
|||
pub fn mount_style(id: &str, content: &'static str) {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::html::style;
|
||||
use leptos_meta::use_head;
|
||||
let meta = use_head();
|
||||
let style_el = style().attr("data-thaw-id", id).child(content);
|
||||
meta.tags.register(format!("leptos-thaw-{id}").into(), style_el.into_any());
|
||||
use leptos::{tachys::view::Render, view};
|
||||
use leptos_meta::Style;
|
||||
|
||||
let _ = view! {
|
||||
<Style attr:data-thaw-id=id>
|
||||
{content}
|
||||
</Style>
|
||||
};
|
||||
} else {
|
||||
use leptos::prelude::document;
|
||||
let head = document().head().expect("head no exist");
|
||||
|
@ -15,9 +18,6 @@ pub fn mount_style(id: &str, content: &'static str) {
|
|||
.query_selector(&format!("style[data-thaw-id=\"{id}\"]"))
|
||||
.expect("query style element error");
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
let _ = leptos::leptos_dom::HydrationCtx::id();
|
||||
|
||||
if style.is_some() {
|
||||
return;
|
||||
}
|
||||
|
@ -35,12 +35,14 @@ pub fn mount_style(id: &str, content: &'static str) {
|
|||
pub fn mount_dynamic_style<T: Fn() -> String + Send + Sync + 'static>(id: String, f: T) {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::html::style;
|
||||
use leptos_meta::use_head;
|
||||
let meta = use_head();
|
||||
let content = leptos::untrack(|| f());
|
||||
let style_el = style().attr("data-thaw-id", id).child(content);
|
||||
meta.tags.register(format!("leptos-thaw-{id}").into(), style_el.into_any());
|
||||
use leptos::{tachys::view::Render, view};
|
||||
use leptos_meta::Style;
|
||||
|
||||
let _ = view! {
|
||||
<Style attr:data-thaw-id=id>
|
||||
{f()}
|
||||
</Style>
|
||||
};
|
||||
} else {
|
||||
use leptos::prelude::document;
|
||||
use send_wrapper::SendWrapper;
|
||||
|
@ -58,9 +60,6 @@ pub fn mount_dynamic_style<T: Fn() -> String + Send + Sync + 'static>(id: String
|
|||
style
|
||||
});
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
let _ = leptos::leptos_dom::HydrationCtx::id();
|
||||
|
||||
let style = SendWrapper::new(style);
|
||||
leptos::prelude::Effect::new_isomorphic(move |_| {
|
||||
let content = f();
|
||||
|
|
|
@ -19,15 +19,3 @@ pub use optional_prop::OptionalProp;
|
|||
pub use signals::*;
|
||||
pub use throttle::throttle;
|
||||
pub use time::now_date;
|
||||
|
||||
pub fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use leptos::leptos_dom::HydrationCtx;
|
||||
HydrationCtx::with_hydration_off(f)
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue