1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-11-21 19:41:09 +00:00

Use cargo-leptos, make server-side rendering work

This commit is contained in:
Felix Ableitner 2024-10-28 15:46:51 +01:00
parent 04b40a32f5
commit fceead8062
17 changed files with 126 additions and 178 deletions

View file

@ -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:

View file

@ -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.

View file

@ -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

View file

@ -1,5 +0,0 @@
[build]
filehash = false
target = "assets/index.html"
dist = "assets/dist"
minify = "on_release"

View file

@ -5,7 +5,7 @@
href=".."
data-bin="ibis"
data-cargo-no-default-features
data-cargo-features="csr,hydrate" />
data-cargo-features="hydrate" />
</head>
<body></body>
</html>

View file

@ -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(())
}

View file

@ -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

View file

@ -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
)

View file

@ -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<Router<()>> {
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<HeaderMap> = 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<LeptosOptions>,
req: Request<Body>,
) -> Result<Response<Body>, (StatusCode, String)> {
let root = options.site_root.clone();
let (parts, body) = req.into_parts();
async fn ibis_css() -> MyResult<(HeaderMap, Response<Body>)> {
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<impl IntoResponse> {
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<impl IntoResponse> {
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<String>) -> MyResult<impl IntoResponse> {
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<Body>,
root: &str,
) -> Result<Response<Body>, (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}"),
)),
}
}

View file

@ -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

View file

@ -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<SocketAddr>) -> MyResult<()> {
let manager = ConnectionManager::<PgConnection>::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(())

View file

@ -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

View file

@ -91,9 +91,9 @@ pub fn App() -> impl IntoView {
view! {
<>
<Stylesheet id="simple" href="/assets/simple.css" />
<Stylesheet id="ibis" href="/assets/ibis.css" />
<Stylesheet id="katex" href="/assets/katex.min.css" />
<Stylesheet id="simple" href="/simple.css" />
<Stylesheet id="ibis" href="/ibis.css" />
<Stylesheet id="katex" href="/katex.min.css" />
<Router>
<Nav />
<main>

View file

@ -11,7 +11,11 @@ pub mod pages;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {}
pub fn hydrate() {
use crate::frontend::app::App;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
fn article_link(article: &DbArticle) -> String {
if article.local {

View file

@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
use ibis_lib::backend::config::IbisConfig;
pub async fn main() -> ibis::backend::error::MyResult<()> {
use ibis::backend::config::IbisConfig;
use log::LevelFilter;
if std::env::args().collect::<Vec<_>>().get(1) == Some(&"--print-config".to_string()) {
@ -16,10 +16,11 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
.init();
let ibis_config = IbisConfig::read()?;
ibis_lib::backend::start(ibis_config).await?;
ibis::backend::start(ibis_config, None).await?;
Ok(())
}
/*
#[cfg(not(feature = "ssr"))]
fn main() {
use ibis_lib::frontend::app::App;
@ -31,3 +32,4 @@ fn main() {
view! { <App /> }
});
}
*/

View file

@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use ibis_lib::{
use ibis::{
backend::{
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
start,
@ -21,7 +21,7 @@ use std::{
thread::{sleep, spawn},
time::Duration,
};
use tokio::task::JoinHandle;
use tokio::{join, task::JoinHandle};
use tracing::log::LevelFilter;
pub struct TestData {
@ -66,11 +66,13 @@ impl TestData {
j.join().unwrap();
}
Self {
alpha: IbisInstance::start(alpha_db_path, port_alpha, "alpha").await,
beta: IbisInstance::start(beta_db_path, port_beta, "beta").await,
gamma: IbisInstance::start(gamma_db_path, port_gamma, "gamma").await,
}
let (alpha, beta, gamma) = join!(
IbisInstance::start(alpha_db_path, port_alpha, "alpha"),
IbisInstance::start(beta_db_path, port_beta, "beta"),
IbisInstance::start(gamma_db_path, port_gamma, "gamma")
);
Self { alpha, beta, gamma }
}
pub fn stop(self) -> MyResult<()> {
@ -115,23 +117,26 @@ impl IbisInstance {
async fn start(db_path: String, port: i32, username: &str) -> Self {
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
let hostname = format!("localhost:{port}");
let bind = format!("127.0.0.1:{port}").parse().unwrap();
let hostname = format!("127.0.0.1:{port}");
let domain = format!("localhost:{port}");
let config = IbisConfig {
bind,
database: IbisConfigDatabase {
connection_url,
..Default::default()
},
registration_open: true,
federation: IbisConfigFederation {
domain: hostname.clone(),
domain: domain.clone(),
..Default::default()
},
..Default::default()
};
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
let api_client = ApiClient::new(client, Some(domain));
let handle = tokio::task::spawn(async move {
start(config).await.unwrap();
start(config, Some(hostname.parse().unwrap()))
.await
.unwrap();
});
// wait a moment for the backend to start
tokio::time::sleep(Duration::from_millis(5000)).await;
@ -139,8 +144,6 @@ impl IbisInstance {
username: username.to_string(),
password: "hunter2".to_string(),
};
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
let api_client = ApiClient::new(client, Some(hostname));
api_client.register(form).await.unwrap();
Self {
api_client,

View file

@ -1,11 +1,9 @@
#![allow(clippy::unwrap_used)]
extern crate ibis_lib;
mod common;
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
use ibis_lib::{
use ibis::{
common::{
utils::extract_domain,
ArticleView,