From fceead8062d573acd6feaee87f73b3dfd88df882 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 28 Oct 2024 15:46:51 +0100 Subject: [PATCH] Use cargo-leptos, make server-side rendering work --- .woodpecker.yml | 8 --- Cargo.toml | 40 ++++++++++----- README.md | 4 +- Trunk.toml | 5 -- assets/index.html | 2 +- build.rs | 20 -------- config/defaults.toml | 3 -- scripts/watch.sh | 15 ------ src/backend/assets.rs | 116 ++++++++++++++++++------------------------ src/backend/config.rs | 5 -- src/backend/mod.rs | 24 +++++---- src/frontend/api.rs | 7 ++- src/frontend/app.rs | 6 +-- src/frontend/mod.rs | 6 ++- src/main.rs | 8 +-- tests/common.rs | 31 ++++++----- tests/test.rs | 4 +- 17 files changed, 126 insertions(+), 178 deletions(-) delete mode 100644 Trunk.toml delete mode 100644 build.rs delete mode 100755 scripts/watch.sh diff --git a/.woodpecker.yml b/.woodpecker.yml index 534709e..b870e58 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -50,14 +50,6 @@ steps: - diesel print-schema --config-file=diesel.toml > tmp.schema - diff tmp.schema src/backend/database/schema.rs - frontend_wasm_build: - image: *rust_image - environment: - CARGO_HOME: .cargo_home - commands: - - "rustup target add wasm32-unknown-unknown" - - "cargo check --target wasm32-unknown-unknown --features csr,hydrate --no-default-features" - cargo_clippy: image: *rust_image environment: diff --git a/Cargo.toml b/Cargo.toml index ba166dc..c7a6419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,29 @@ ssr = [ "leptos/ssr", "leptos-use/ssr", ] -csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "katex/wasm-js"] -hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +hydrate = [ + "leptos/hydrate", + "leptos_meta/hydrate", + "leptos_router/hydrate", + "katex/wasm-js", +] + +# This profile significantly speeds up build time. If debug info is needed you can comment the line +# out temporarily, but make sure to leave this in the main branch. +[profile.dev] +debug = 0 + +[profile.release] +lto = "thin" +strip = true + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" [lints.clippy] dbg_macro = "deny" @@ -95,19 +116,10 @@ pretty_assertions = "1.4.1" [package.metadata.leptos] output-name = "ibis" +assets-dir = "assets" bin-features = ["ssr"] -lib-features = ["csr"] +lib-features = ["hydrate"] +lib-profile-release = "wasm-release" [lib] -name = "ibis_lib" crate-type = ["cdylib", "rlib"] - -# This profile significantly speeds up build time. If debug info is needed you can comment the line -# out temporarily, but make sure to leave this in the main branch. -[profile.dev] -debug = 0 - -[profile.release] -lto = "thin" -strip = true # Automatically strip symbols from the binary. -#opt-level = "z" # Optimize for size. diff --git a/README.md b/README.md index b4e0dc4..5cfca71 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ psql -c "CREATE USER ibis WITH PASSWORD 'ibis' SUPERUSER;" -U postgres psql -c "CREATE DATABASE ibis WITH OWNER ibis;" -U postgres ``` -You need to install [cargo](https://rustup.rs/), [trunk](https://trunkrs.dev) and [cargo watch](https://github.com/watchexec/cargo-watch). Run `./scripts/watch.sh` which automatically rebuilds the project after changes. Then open the site at [127.0.0.1:8080](http://127.0.0.1:8080/). Then login with user `ibis` and password `ibis`. +You need to install [cargo](https://rustup.rs/), and [cargo-leptos](https://github.com/leptos-rs/cargo-leptos). Run `cargo leptos watch` which automatically rebuilds the project after changes. Then open the site at [localhost:3000](http://localhost:3000/). You can login with user `ibis` and password `ibis`. -By default the frontend runs on port 8080, which can be changed with env var `TRUNK_SERVE_PORT`. The backend port is 8081 and can be changed with `IBIS_BACKEND_PORT`. +By default the frontend runs on port 3000, which can be changed with env var `TRUNK_SERVE_PORT`. The backend port is 8081 and can be changed with `IBIS_BACKEND_PORT`. ## Federation diff --git a/Trunk.toml b/Trunk.toml deleted file mode 100644 index 32505a0..0000000 --- a/Trunk.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -filehash = false -target = "assets/index.html" -dist = "assets/dist" -minify = "on_release" diff --git a/assets/index.html b/assets/index.html index f2069ac..388b99f 100644 --- a/assets/index.html +++ b/assets/index.html @@ -5,7 +5,7 @@ href=".." data-bin="ibis" data-cargo-no-default-features - data-cargo-features="csr,hydrate" /> + data-cargo-features="hydrate" /> \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index b54887b..0000000 --- a/build.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::{ - fs::{create_dir_all, File}, - io::Result, - path::Path, -}; - -/// Create placeholders for wasm files so that `cargo check` etc work without explicitly building -/// frontend. -fn main() -> Result<()> { - create_dir_all("assets/dist/")?; - let js = "assets/dist/ibis.js"; - if !Path::new(js).exists() { - File::create(js)?; - } - let wasm = "assets/dist/ibis_bg.wasm"; - if !Path::new(wasm).exists() { - File::create(wasm)?; - } - Ok(()) -} diff --git a/config/defaults.toml b/config/defaults.toml index cf920fb..83f7333 100644 --- a/config/defaults.toml +++ b/config/defaults.toml @@ -1,6 +1,3 @@ -# Address where ibis should listen for incoming requests -bind = "127.0.0.1:8081" - # Whether users can create new accounts registration_open = true diff --git a/scripts/watch.sh b/scripts/watch.sh deleted file mode 100755 index a2ef680..0000000 --- a/scripts/watch.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -set -e - -IBIS__BIND="${IBIS_BIND:-"127.0.0.1:8081"}" - -killall trunk || true - -# run processes in parallel -# https://stackoverflow.com/a/52033580 -(trap 'kill 0' INT; - # start frontend - CARGO_TARGET_DIR=target/frontend trunk serve -w src/frontend/ -w assets/ --proxy-backend http://$IBIS__BIND & - # start backend, with separate target folder to avoid rebuilds from arch change - cargo watch --ignore assets/ibis.css --exec run -) diff --git a/src/backend/assets.rs b/src/backend/assets.rs index 4ab0d96..c38f913 100644 --- a/src/backend/assets.rs +++ b/src/backend/assets.rs @@ -1,79 +1,61 @@ -use super::error::MyResult; -use anyhow::anyhow; +use crate::frontend::app::App; use axum::{ body::Body, - extract::Path, + extract::{Request, State}, + http::StatusCode, response::{IntoResponse, Response}, - routing::get, - Router, }; use axum_macros::debug_handler; -use include_dir::include_dir; -use once_cell::sync::OnceCell; -use reqwest::header::HeaderMap; -use std::fs::read_to_string; +use leptos::LeptosOptions; +use tower::ServiceExt; +use tower_http::services::ServeDir; -pub fn asset_routes() -> MyResult> { - Ok(Router::new() - .route("/assets/ibis.css", get(ibis_css)) - .route( - "/assets/simple.css", - get((css_headers(), include_str!("../../assets/simple.css"))), - ) - .route( - "/assets/katex.min.css", - get((css_headers(), include_str!("../../assets/katex.min.css"))), - ) - .route("/assets/fonts/*font", get(get_font)) - .route( - "/assets/index.html", - get(include_str!("../../assets/index.html")), - ) - .route("/pkg/ibis.js", get(serve_js)) - .route("/pkg/ibis_bg.wasm", get(serve_wasm))) -} +// from https://github.com/leptos-rs/start-axum -fn css_headers() -> HeaderMap { - static INSTANCE: OnceCell = OnceCell::new(); - INSTANCE - .get_or_init(|| { - let mut css_headers = HeaderMap::new(); - let val = "text/css".parse().expect("valid header value"); - css_headers.insert("Content-Type", val); - css_headers - }) - .clone() -} +#[debug_handler] +pub async fn file_and_error_handler( + State(options): State, + req: Request, +) -> Result, (StatusCode, String)> { + let root = options.site_root.clone(); + let (parts, body) = req.into_parts(); -async fn ibis_css() -> MyResult<(HeaderMap, Response)> { - let res = if cfg!(debug_assertions) { - read_to_string("assets/ibis.css")?.into_response() + let mut static_parts = parts.clone(); + static_parts.headers.clear(); + if let Some(encodings) = parts.headers.get("accept-encoding") { + static_parts + .headers + .insert("accept-encoding", encodings.clone()); + } + + let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root).await?; + + if res.status() == StatusCode::OK { + Ok(res.into_response()) } else { - include_str!("../../assets/ibis.css").into_response() - }; - Ok((css_headers(), res)) + let handler = leptos_axum::render_app_to_stream(options.to_owned(), App); + Ok(handler(Request::from_parts(parts, body)) + .await + .into_response()) + } } -#[debug_handler] -async fn serve_js() -> MyResult { - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/javascript".parse()?); - let content = include_str!("../../assets/dist/ibis.js"); - Ok((headers, content)) -} - -#[debug_handler] -async fn serve_wasm() -> MyResult { - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/wasm".parse()?); - let content = include_bytes!("../../assets/dist/ibis_bg.wasm"); - Ok((headers, content)) -} - -#[debug_handler] -async fn get_font(Path(font): Path) -> MyResult { - let headers = HeaderMap::new(); - let font_dir = include_dir!("assets/fonts"); - let file = font_dir.get_file(font).ok_or(anyhow!("invalid font"))?; - Ok((headers, file.contents())) +async fn get_static_file( + request: Request, + root: &str, +) -> Result, (StatusCode, String)> { + // `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) + .precompressed_gzip() + .precompressed_br() + .oneshot(request) + .await + { + Ok(res) => Ok(res.into_response()), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error serving files: {err}"), + )), + } } diff --git a/src/backend/config.rs b/src/backend/config.rs index bd4e758..5e48236 100644 --- a/src/backend/config.rs +++ b/src/backend/config.rs @@ -3,15 +3,10 @@ use config::Config; use doku::Document; use serde::Deserialize; use smart_default::SmartDefault; -use std::net::SocketAddr; #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[serde(default)] pub struct IbisConfig { - /// Address where ibis should listen for incoming requests - #[default("127.0.0.1:8081".parse().expect("parse config bind"))] - #[doku(as = "String", example = "127.0.0.1:8081")] - pub bind: SocketAddr, /// Details about the PostgreSQL database connection pub database: IbisConfigDatabase, /// Whether users can create new accounts diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f119946..93ad456 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -22,7 +22,7 @@ use activitypub_federation::{ http_signatures::generate_actor_keypair, }; use api::api_routes; -use assets::asset_routes; +use assets::file_and_error_handler; use axum::{ body::Body, http::{HeaderValue, Request}, @@ -38,9 +38,10 @@ use diesel::{ PgConnection, }; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use leptos::leptos_config::get_config_from_str; +use leptos::get_configuration; use leptos_axum::{generate_route_list, LeptosRoutes}; use log::info; +use std::net::SocketAddr; use tokio::net::TcpListener; use tower_http::cors::CorsLayer; use tower_layer::Layer; @@ -58,7 +59,7 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); const FEDERATION_ROUTES_PREFIX: &str = "/federation_routes"; -pub async fn start(config: IbisConfig) -> MyResult<()> { +pub async fn start(config: IbisConfig, override_hostname: Option) -> MyResult<()> { let manager = ConnectionManager::::new(&config.database.connection_url); let db_pool = Pool::builder() .max_size(config.database.pool_size) @@ -82,15 +83,18 @@ pub async fn start(config: IbisConfig) -> MyResult<()> { setup(&data.to_request_data()).await?; } - let mut conf = get_config_from_str(include_str!("../../Cargo.toml"))?; - conf.site_addr = data.config.bind; + let leptos_options = get_configuration(Some("Cargo.toml")).await?.leptos_options; + let mut addr = leptos_options.site_addr; + if let Some(override_hostname) = override_hostname { + addr = override_hostname; + } let routes = generate_route_list(App); let config = data.clone(); let app = Router::new() - .leptos_routes(&conf, routes, App) - .with_state(conf) - .nest("", asset_routes()?) + .leptos_routes(&leptos_options, routes, App) + .fallback(file_and_error_handler) + .with_state(leptos_options) .nest(FEDERATION_ROUTES_PREFIX, federation_routes()) .nest("/api/v1", api_routes()) .nest("", nodeinfo::config()) @@ -102,8 +106,8 @@ pub async fn start(config: IbisConfig) -> MyResult<()> { let middleware = axum::middleware::from_fn(federation_routes_middleware); let app_with_middleware = middleware.layer(app); - info!("Listening on {}", &data.config.bind); - let listener = TcpListener::bind(&data.config.bind).await?; + info!("Listening on {}", &addr); + let listener = TcpListener::bind(&addr).await?; axum::serve(listener, app_with_middleware.into_make_service()).await?; Ok(()) diff --git a/src/frontend/api.rs b/src/frontend/api.rs index 0792ac2..12419bf 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -47,10 +47,9 @@ impl ApiClient { } #[cfg(feature = "ssr")] { - hostname = crate::backend::config::IbisConfig::read() - .unwrap() - .bind - .to_string(); + use leptos::leptos_config::get_config_from_str; + let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap(); + hostname = leptos_options.site_addr.to_string(); ssl = false; } // required for tests diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 3efec9b..093d4dc 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -91,9 +91,9 @@ pub fn App() -> impl IntoView { view! { <> - - - + + +