mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-22 19:41:09 +00:00
Use cargo-leptos, make server-side rendering work
This commit is contained in:
parent
b4e54c914d
commit
fc3e94bea0
15 changed files with 101 additions and 153 deletions
35
Cargo.toml
35
Cargo.toml
|
@ -21,8 +21,24 @@ ssr = [
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos-use/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","katex/wasm-js"]
|
||||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
|
||||||
|
# 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]
|
[lints.clippy]
|
||||||
dbg_macro = "deny"
|
dbg_macro = "deny"
|
||||||
|
@ -95,19 +111,10 @@ pretty_assertions = "1.4.1"
|
||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
output-name = "ibis"
|
output-name = "ibis"
|
||||||
|
assets-dir = "assets"
|
||||||
bin-features = ["ssr"]
|
bin-features = ["ssr"]
|
||||||
lib-features = ["csr"]
|
lib-features = ["hydrate"]
|
||||||
|
lib-profile-release = "wasm-release"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "ibis_lib"
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
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
|
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
|
## Federation
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[build]
|
|
||||||
filehash = false
|
|
||||||
target = "assets/index.html"
|
|
||||||
dist = "assets/dist"
|
|
||||||
minify = "on_release"
|
|
|
@ -5,7 +5,7 @@
|
||||||
href=".."
|
href=".."
|
||||||
data-bin="ibis"
|
data-bin="ibis"
|
||||||
data-cargo-no-default-features
|
data-cargo-no-default-features
|
||||||
data-cargo-features="csr,hydrate" />
|
data-cargo-features="hydrate" />
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</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,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,62 @@
|
||||||
use super::error::MyResult;
|
use crate::frontend::app::App;
|
||||||
use anyhow::anyhow;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::Path,
|
extract::{Request, State},
|
||||||
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
|
||||||
Router,
|
|
||||||
};
|
};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
use include_dir::include_dir;
|
use leptos::LeptosOptions;
|
||||||
use once_cell::sync::OnceCell;
|
use tower::ServiceExt;
|
||||||
use reqwest::header::HeaderMap;
|
use tower_http::services::ServeDir;
|
||||||
use std::fs::read_to_string;
|
|
||||||
|
|
||||||
pub fn asset_routes() -> MyResult<Router<()>> {
|
// from https://github.com/leptos-rs/start-axum
|
||||||
Ok(Router::new()
|
|
||||||
.route("/assets/ibis.css", get(ibis_css))
|
#[debug_handler]
|
||||||
.route(
|
pub async fn file_and_error_handler(
|
||||||
"/assets/simple.css",
|
State(options): State<LeptosOptions>,
|
||||||
get((css_headers(), include_str!("../../assets/simple.css"))),
|
req: Request<Body>,
|
||||||
)
|
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||||
.route(
|
let root = options.site_root.clone();
|
||||||
"/assets/katex.min.css",
|
let (parts, body) = req.into_parts();
|
||||||
get((css_headers(), include_str!("../../assets/katex.min.css"))),
|
|
||||||
)
|
let mut static_parts = parts.clone();
|
||||||
.route("/assets/fonts/*font", get(get_font))
|
static_parts.headers.clear();
|
||||||
.route(
|
if let Some(encodings) = parts.headers.get("accept-encoding") {
|
||||||
"/assets/index.html",
|
static_parts
|
||||||
get(include_str!("../../assets/index.html")),
|
.headers
|
||||||
)
|
.insert("accept-encoding", encodings.clone());
|
||||||
.route("/pkg/ibis.js", get(serve_js))
|
|
||||||
.route("/pkg/ibis_bg.wasm", get(serve_wasm)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn css_headers() -> HeaderMap {
|
let res = get_static_file(Request::from_parts(static_parts, Body::empty()), &root)
|
||||||
static INSTANCE: OnceCell<HeaderMap> = OnceCell::new();
|
.await?;
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ibis_css() -> MyResult<(HeaderMap, Response<Body>)> {
|
if res.status() == StatusCode::OK {
|
||||||
let res = if cfg!(debug_assertions) {
|
Ok(res.into_response())
|
||||||
read_to_string("assets/ibis.css")?.into_response()
|
|
||||||
} else {
|
} else {
|
||||||
include_str!("../../assets/ibis.css").into_response()
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
|
||||||
};
|
Ok(handler(Request::from_parts(parts, body))
|
||||||
Ok((css_headers(), res))
|
.await
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
async fn get_static_file(
|
||||||
async fn serve_js() -> MyResult<impl IntoResponse> {
|
request: Request<Body>,
|
||||||
let mut headers = HeaderMap::new();
|
root: &str,
|
||||||
headers.insert("Content-Type", "application/javascript".parse()?);
|
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||||
let content = include_str!("../../assets/dist/ibis.js");
|
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||||
Ok((headers, content))
|
// 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}"),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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()))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,10 @@ use config::Config;
|
||||||
use doku::Document;
|
use doku::Document;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smart_default::SmartDefault;
|
use smart_default::SmartDefault;
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct IbisConfig {
|
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
|
/// Details about the PostgreSQL database connection
|
||||||
pub database: IbisConfigDatabase,
|
pub database: IbisConfigDatabase,
|
||||||
/// Whether users can create new accounts
|
/// Whether users can create new accounts
|
||||||
|
|
|
@ -22,7 +22,7 @@ use activitypub_federation::{
|
||||||
http_signatures::generate_actor_keypair,
|
http_signatures::generate_actor_keypair,
|
||||||
};
|
};
|
||||||
use api::api_routes;
|
use api::api_routes;
|
||||||
use assets::asset_routes;
|
use assets::{file_and_error_handler};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{HeaderValue, Request},
|
http::{HeaderValue, Request},
|
||||||
|
@ -82,15 +82,15 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
|
||||||
setup(&data.to_request_data()).await?;
|
setup(&data.to_request_data()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut conf = get_config_from_str(include_str!("../../Cargo.toml"))?;
|
let leptos_options = get_config_from_str(include_str!("../../Cargo.toml"))?;
|
||||||
conf.site_addr = data.config.bind;
|
let addr = leptos_options.site_addr;
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
let config = data.clone();
|
let config = data.clone();
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.leptos_routes(&conf, routes, App)
|
.leptos_routes(&leptos_options, routes, App)
|
||||||
.with_state(conf)
|
.fallback(file_and_error_handler)
|
||||||
.nest("", asset_routes()?)
|
.with_state(leptos_options)
|
||||||
.nest(FEDERATION_ROUTES_PREFIX, federation_routes())
|
.nest(FEDERATION_ROUTES_PREFIX, federation_routes())
|
||||||
.nest("/api/v1", api_routes())
|
.nest("/api/v1", api_routes())
|
||||||
.nest("", nodeinfo::config())
|
.nest("", nodeinfo::config())
|
||||||
|
@ -102,8 +102,8 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
|
||||||
let middleware = axum::middleware::from_fn(federation_routes_middleware);
|
let middleware = axum::middleware::from_fn(federation_routes_middleware);
|
||||||
let app_with_middleware = middleware.layer(app);
|
let app_with_middleware = middleware.layer(app);
|
||||||
|
|
||||||
info!("Listening on {}", &data.config.bind);
|
info!("Listening on {}", &addr);
|
||||||
let listener = TcpListener::bind(&data.config.bind).await?;
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
axum::serve(listener, app_with_middleware.into_make_service()).await?;
|
axum::serve(listener, app_with_middleware.into_make_service()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -47,10 +47,9 @@ impl ApiClient {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
{
|
{
|
||||||
hostname = crate::backend::config::IbisConfig::read()
|
use leptos::leptos_config::get_config_from_str;
|
||||||
.unwrap()
|
let leptos_options = get_config_from_str(include_str!("../../Cargo.toml")).unwrap();
|
||||||
.bind
|
hostname = leptos_options.site_addr.to_string();
|
||||||
.to_string();
|
|
||||||
ssl = false;
|
ssl = false;
|
||||||
}
|
}
|
||||||
// required for tests
|
// required for tests
|
||||||
|
|
|
@ -91,9 +91,9 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<>
|
<>
|
||||||
<Stylesheet id="simple" href="/assets/simple.css" />
|
<Stylesheet id="simple" href="/simple.css" />
|
||||||
<Stylesheet id="ibis" href="/assets/ibis.css" />
|
<Stylesheet id="ibis" href="/ibis.css" />
|
||||||
<Stylesheet id="katex" href="/assets/katex.min.css" />
|
<Stylesheet id="katex" href="/katex.min.css" />
|
||||||
<Router>
|
<Router>
|
||||||
<Nav />
|
<Nav />
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -11,7 +11,11 @@ pub mod pages;
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[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 {
|
fn article_link(article: &DbArticle) -> String {
|
||||||
if article.local {
|
if article.local {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
|
pub async fn main() -> ibis::backend::error::MyResult<()> {
|
||||||
use ibis_lib::backend::config::IbisConfig;
|
use ibis::backend::config::IbisConfig;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
|
||||||
if std::env::args().collect::<Vec<_>>().get(1) == Some(&"--print-config".to_string()) {
|
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();
|
.init();
|
||||||
|
|
||||||
let ibis_config = IbisConfig::read()?;
|
let ibis_config = IbisConfig::read()?;
|
||||||
ibis_lib::backend::start(ibis_config).await?;
|
ibis::backend::start(ibis_config).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
fn main() {
|
fn main() {
|
||||||
use ibis_lib::frontend::app::App;
|
use ibis_lib::frontend::app::App;
|
||||||
|
@ -31,3 +32,4 @@ fn main() {
|
||||||
view! { <App /> }
|
view! { <App /> }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
use ibis_lib::{
|
use ibis::{
|
||||||
backend::{
|
backend::{
|
||||||
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation},
|
||||||
start,
|
start,
|
||||||
|
@ -116,9 +116,7 @@ impl IbisInstance {
|
||||||
async fn start(db_path: String, port: i32, username: &str) -> Self {
|
async fn start(db_path: String, port: i32, username: &str) -> Self {
|
||||||
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
|
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
|
||||||
let hostname = format!("localhost:{port}");
|
let hostname = format!("localhost:{port}");
|
||||||
let bind = format!("127.0.0.1:{port}").parse().unwrap();
|
|
||||||
let config = IbisConfig {
|
let config = IbisConfig {
|
||||||
bind,
|
|
||||||
database: IbisConfigDatabase {
|
database: IbisConfigDatabase {
|
||||||
connection_url,
|
connection_url,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
extern crate ibis_lib;
|
extern crate ibis;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
|
use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT};
|
||||||
use ibis_lib::{
|
use ibis::{
|
||||||
common::{
|
common::{
|
||||||
utils::extract_domain,
|
utils::extract_domain,
|
||||||
ArticleView,
|
ArticleView,
|
||||||
|
|
Loading…
Reference in a new issue