mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 00:51:09 +00:00
Use cargo-leptos, make server-side rendering work
This commit is contained in:
parent
04b40a32f5
commit
fceead8062
17 changed files with 126 additions and 178 deletions
|
@ -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:
|
||||
|
|
40
Cargo.toml
40
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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[build]
|
||||
filehash = false
|
||||
target = "assets/index.html"
|
||||
dist = "assets/dist"
|
||||
minify = "on_release"
|
|
@ -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>
|
20
build.rs
20
build.rs
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 /> }
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue