feat: add feature flat to tuono_lib

This commit is contained in:
Valerio Ageno
2024-12-14 12:10:33 +01:00
parent 4abc163a59
commit edb21ab969
9 changed files with 320 additions and 197 deletions
+30 -16
View File
@@ -15,29 +15,43 @@ include = [
"Cargo.toml"
]
[features]
full = ["api", "ssr"]
api = []
ssr = [
"dep:ssr_rs",
"dep:erased-serde",
"dep:serde_json",
"dep:reqwest",
"dep:tokio-tungstenite",
"dep:tungstenite",
"dep:futures-util"
]
[dependencies]
ssr_rs = "0.7.0"
axum = {version = "0.7.5", features = ["json", "ws"]}
axum-extra = {version = "0.9.6", features = ["cookie"]}
tokio = { version = "1.37.0", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
erased-serde = "0.4.5"
serde_json = "1.0"
serde_urlencoded = "0.7.1"
reqwest = {version = "0.12.4", features = ["json", "stream"]}
once_cell = "1.19.0"
regex = "1.10.5"
either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.16.2"}
# Match the same version used by axum
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tungstenite = "0.24.0"
regex = "1.10.5"
http = "1.1.0"
pin-project = "1.1.7"
tower = "0.5.1"
colored = "2.1.0"
# Check if serde is mandatory for API only project
serde = { version = "1.0.202", features = ["derive"] }
# Check if once_cell is mandatory for API only project
once_cell = "1.19.0"
ssr_rs = { version = "0.7.0", optional = true}
erased-serde = {version = "0.4.5", optional = true}
serde_json = { version = "1.0", optional = true}
serde_urlencoded = {version = "0.7.1" }
reqwest = {version = "0.12.4", features = ["json", "stream"], optional = true}
# Match the same version used internally by axum
tokio-tungstenite = {version = "0.24.0", optional = true}
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"], optional = true}
tungstenite = {version = "0.24.0", optional = true}
tower-http = {version = "0.6.0", features = ["fs"]}
+21 -17
View File
@@ -1,24 +1,28 @@
use crate::{ssr::Js, Payload};
use axum::extract::{Path, Request};
use axum::response::Html;
use std::collections::HashMap;
#[cfg(feature = "ssr")]
mod ssr {
use crate::ssr::Js;
use crate::Payload;
use axum::extract::{Path, Request};
use axum::response::Html;
use std::collections::HashMap;
pub async fn catch_all(
Path(params): Path<HashMap<String, String>>,
request: Request,
) -> Html<String> {
let pathname = request.uri();
let headers = request.headers();
pub async fn catch_all(
Path(params): Path<HashMap<String, String>>,
request: Request,
) -> Html<String> {
let pathname = request.uri();
let headers = request.headers();
let req = crate::Request::new(pathname.to_owned(), headers.to_owned(), params);
let req = crate::Request::new(pathname.to_owned(), headers.to_owned(), params);
// TODO: remove unwrap
let payload = Payload::new(&req, &"").client_payload().unwrap();
// TODO: remove unwrap
let payload = Payload::new(&req, &"").client_payload().unwrap();
let result = Js::render_to_string(Some(&payload));
let result = Js::render_to_string(Some(&payload));
match result {
Ok(html) => Html(html),
_ => Html("500 internal server error".to_string()),
match result {
Ok(html) => Html(html),
_ => Html("500 internal server error".to_string()),
}
}
}
+26 -10
View File
@@ -1,19 +1,10 @@
mod catch_all;
mod logger;
mod manifest;
mod mode;
mod payload;
mod request;
mod response;
mod server;
mod ssr;
mod vite_reverse_proxy;
mod vite_websocket_proxy;
pub use mode::Mode;
pub use payload::Payload;
pub use request::Request;
pub use response::{Props, Response};
pub use server::Server;
pub use tuono_lib_macros::{api, handler};
@@ -21,3 +12,28 @@ pub use tuono_lib_macros::{api, handler};
pub use axum;
pub use axum_extra::extract::cookie;
pub use tokio;
mod request;
pub use request::Request;
#[cfg(feature = "ssr")]
#[cfg(feature = "ssr")]
mod manifest;
mod response;
#[cfg(feature = "ssr")]
mod payload;
#[cfg(feature = "ssr")]
pub use payload::Payload;
#[cfg(feature = "ssr")]
pub use response::response::{Props, Response};
#[cfg(feature = "ssr")]
mod ssr;
#[cfg(feature = "ssr")]
mod vite_reverse_proxy;
#[cfg(feature = "ssr")]
mod vite_websocket_proxy;
+148 -145
View File
@@ -1,160 +1,163 @@
use crate::Request;
use crate::{ssr::Js, Payload};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use axum::Json;
use axum_extra::extract::cookie::{Cookie, CookieJar};
use erased_serde::Serialize;
#[cfg(feature = "ssr")]
pub(crate) mod response {
use crate::Request;
use crate::{ssr::Js, Payload};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use axum::Json;
use axum_extra::extract::cookie::{Cookie, CookieJar};
use erased_serde::Serialize;
pub struct Props {
data: Box<dyn Serialize>,
http_code: StatusCode,
cookies: CookieJar,
}
pub enum Response {
Redirect(String),
Props(Props),
// TODO: improve this tuple to support a more generic IntoResponse
Custom((StatusCode, HeaderMap, String)),
}
#[derive(serde::Serialize)]
struct JsonResponseInfo {
redirect_destination: Option<String>,
}
impl JsonResponseInfo {
fn new(redirect_destination: Option<String>) -> JsonResponseInfo {
JsonResponseInfo {
redirect_destination,
}
}
}
#[derive(serde::Serialize)]
struct JsonResponse<'a> {
data: Option<&'a dyn Serialize>,
info: JsonResponseInfo,
}
impl<'a> JsonResponse<'a> {
fn new(props: &'a dyn Serialize) -> Self {
JsonResponse {
data: Some(props),
info: JsonResponseInfo::new(None),
}
pub struct Props {
data: Box<dyn Serialize>,
http_code: StatusCode,
cookies: CookieJar,
}
fn new_redirect(destination: String) -> Self {
JsonResponse {
data: None,
info: JsonResponseInfo::new(Some(destination)),
}
}
}
impl Props {
pub fn new(data: impl Serialize + 'static) -> Self {
Props {
data: Box::new(data),
http_code: StatusCode::OK,
cookies: CookieJar::new(),
}
pub enum Response {
Redirect(String),
Props(Props),
// TODO: improve this tuple to support a more generic IntoResponse
Custom((StatusCode, HeaderMap, String)),
}
pub fn status(&mut self, http_code: StatusCode) {
self.http_code = http_code;
#[derive(serde::Serialize)]
struct JsonResponseInfo {
redirect_destination: Option<String>,
}
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
Props {
data: Box::new(data),
http_code,
cookies: CookieJar::new(),
}
}
pub fn add_cookie(&mut self, cookie: Cookie) {
let jar = self.cookies.clone().add(cookie.into_owned());
self.cookies = jar
}
}
impl Response {
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
match self {
Self::Props(Props {
data,
http_code,
cookies,
}) => {
let payload = Payload::new(&req, data.as_ref()).client_payload().unwrap();
match Js::render_to_string(Some(&payload)) {
Ok(html) => (*http_code, cookies.clone(), Html(html)),
Err(_) => (
*http_code,
cookies.clone(),
Html("500 Internal server error".to_string()),
),
}
.into_response()
impl JsonResponseInfo {
fn new(redirect_destination: Option<String>) -> JsonResponseInfo {
JsonResponseInfo {
redirect_destination,
}
Self::Redirect(to) => Redirect::permanent(to).into_response(),
Self::Custom(response) => response.clone().into_response(),
}
}
pub fn json(&self) -> impl IntoResponse {
match self {
Self::Props(Props {
data,
#[derive(serde::Serialize)]
struct JsonResponse<'a> {
data: Option<&'a dyn Serialize>,
info: JsonResponseInfo,
}
impl<'a> JsonResponse<'a> {
fn new(props: &'a dyn Serialize) -> Self {
JsonResponse {
data: Some(props),
info: JsonResponseInfo::new(None),
}
}
fn new_redirect(destination: String) -> Self {
JsonResponse {
data: None,
info: JsonResponseInfo::new(Some(destination)),
}
}
}
impl Props {
pub fn new(data: impl Serialize + 'static) -> Self {
Props {
data: Box::new(data),
http_code: StatusCode::OK,
cookies: CookieJar::new(),
}
}
pub fn status(&mut self, http_code: StatusCode) {
self.http_code = http_code;
}
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
Props {
data: Box::new(data),
http_code,
cookies,
}) => (
*http_code,
cookies.clone(),
Json(JsonResponse::new(data.as_ref())),
)
.into_response(),
Self::Redirect(destination) => (
StatusCode::PERMANENT_REDIRECT,
Json(JsonResponse::new_redirect(destination.to_string())),
)
.into_response(),
// Custom never needs the "data" response since its scope
// is outside the react domain
Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(),
cookies: CookieJar::new(),
}
}
pub fn add_cookie(&mut self, cookie: Cookie) {
let jar = self.cookies.clone().add(cookie.into_owned());
self.cookies = jar
}
}
impl Response {
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
match self {
Self::Props(Props {
data,
http_code,
cookies,
}) => {
let payload = Payload::new(&req, data.as_ref()).client_payload().unwrap();
match Js::render_to_string(Some(&payload)) {
Ok(html) => (*http_code, cookies.clone(), Html(html)),
Err(_) => (
*http_code,
cookies.clone(),
Html("500 Internal server error".to_string()),
),
}
.into_response()
}
Self::Redirect(to) => Redirect::permanent(to).into_response(),
Self::Custom(response) => response.clone().into_response(),
}
}
pub fn json(&self) -> impl IntoResponse {
match self {
Self::Props(Props {
data,
http_code,
cookies,
}) => (
*http_code,
cookies.clone(),
Json(JsonResponse::new(data.as_ref())),
)
.into_response(),
Self::Redirect(destination) => (
StatusCode::PERMANENT_REDIRECT,
Json(JsonResponse::new_redirect(destination.to_string())),
)
.into_response(),
// Custom never needs the "data" response since its scope
// is outside the react domain
Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_update_the_props_status_and_cookie() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
#[test]
fn should_add_a_cookie_jar() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_update_the_props_status_and_cookie() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
#[test]
fn should_add_a_cookie_jar() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
}
+46 -5
View File
@@ -1,16 +1,28 @@
use crate::manifest::load_manifest;
use crate::mode::{Mode, GLOBAL_MODE};
use axum::routing::{get, Router};
use axum::routing::Router;
use colored::Colorize;
use ssr_rs::Ssr;
use tower_http::services::ServeDir;
use crate::logger::LoggerLayer;
#[cfg(feature = "ssr")]
use crate::{
catch_all::catch_all, logger::LoggerLayer, vite_reverse_proxy::vite_reverse_proxy,
catch_all::catch_all, vite_reverse_proxy::vite_reverse_proxy,
vite_websocket_proxy::vite_websocket_proxy,
};
#[cfg(feature = "ssr")]
use crate::manifest::load_manifest;
#[cfg(feature = "ssr")]
use axum::routing::get;
#[cfg(feature = "ssr")]
use ssr_rs::Ssr;
#[cfg(feature = "ssr")]
use tower_http::services::ServeDir;
#[cfg(feature = "ssr")]
const DEV_PUBLIC_DIR: &str = "public";
#[cfg(feature = "ssr")]
const PROD_PUBLIC_DIR: &str = "out/client";
pub struct Server {
@@ -19,6 +31,7 @@ pub struct Server {
}
impl Server {
#[cfg(feature = "ssr")]
pub fn init(router: Router, mode: Mode) -> Server {
Ssr::create_platform();
@@ -31,6 +44,14 @@ impl Server {
Server { router, mode }
}
#[cfg(not(feature = "ssr"))]
pub fn init(router: Router, mode: Mode) -> Server {
GLOBAL_MODE.set(mode).unwrap();
Server { router, mode }
}
#[cfg(feature = "ssr")]
pub async fn start(&self) {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
@@ -69,4 +90,24 @@ impl Server {
.expect("Failed to serve production server");
}
}
#[cfg(not(feature = "ssr"))]
pub async fn start(&self) {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
if self.mode == Mode::Dev {
println!(" Ready at: {}\n", "http://localhost:3000".blue().bold());
} else {
println!(
" Production server at: {}\n",
"http://localhost:3000".blue().bold()
);
}
let router = self.router.to_owned().layer(LoggerLayer::new());
axum::serve(listener, router)
.await
.expect("Failed to serve production server");
}
}
+4 -4
View File
@@ -36,10 +36,10 @@ struct ProdJs;
impl ProdJs {
thread_local! {
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
Ssr::from(
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
Ssr::from(
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
}
}
+10
View File
@@ -0,0 +1,10 @@
// File automatically generated by tuono
// Do not manually update this file
import 'vite/modulepreload-polyfill'
import { hydrate } from 'tuono/hydration'
import '../src/styles/global.css'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
hydrate(routeTree)
+29
View File
@@ -0,0 +1,29 @@
// File automatically generated
// Do not manually change it
use tuono_lib::{tokio, Mode, Server, axum::Router};
use tuono_lib::axum::routing::get;
use tuono_lib::axum::routing::post;
const MODE: Mode = Mode::Dev;
// MODULE_IMPORTS
#[path="../src/routes/index.rs"]
mod index;
#[tokio::main]
async fn main() {
println!("\n ⚡ Tuono v0.16.1");
let router = Router::new()
// ROUTE_BUILDER
.route("/", get(index::get__tuono_internal_api)).route("/", post(index::post__tuono_internal_api)) ;
Server::init(router, MODE).start().await
}
@@ -0,0 +1,6 @@
// File automatically generated by tuono
// Do not manually update this file
import { routeTree } from './routeTree.gen'
import { serverSideRendering } from 'tuono/ssr'
export const renderFn = serverSideRendering(routeTree)