mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a2144a0d | |||
| adf7ba5391 | |||
| edb21ab969 |
@@ -9,6 +9,7 @@ exclude = [
|
||||
"apps/documentation",
|
||||
"examples/with-mdx",
|
||||
"examples/tuono-app",
|
||||
"examples/tuono-api",
|
||||
"examples/tuono-tutorial",
|
||||
"benches/tuono"
|
||||
]
|
||||
|
||||
@@ -26,6 +26,7 @@ miette = "7.2.0"
|
||||
watchexec-signals = "4.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
toml = "0.8.19"
|
||||
watchexec-supervisor = "3.0.0"
|
||||
glob = "0.3.1"
|
||||
regex = "1.10.4"
|
||||
|
||||
+51
-4
@@ -1,6 +1,8 @@
|
||||
use glob::glob;
|
||||
use glob::GlobError;
|
||||
use http::Method;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
@@ -10,6 +12,7 @@ use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use toml::{Table, Value};
|
||||
|
||||
use crate::route::Route;
|
||||
|
||||
@@ -37,9 +40,10 @@ pub struct App {
|
||||
pub route_map: HashMap<String, Route>,
|
||||
pub base_path: PathBuf,
|
||||
pub has_app_state: bool,
|
||||
pub is_api_only_mode: bool,
|
||||
}
|
||||
|
||||
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||
fn has_app_state(base_path: &PathBuf) -> std::io::Result<bool> {
|
||||
let file = File::open(base_path.join("src/app.rs"))?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
@@ -47,6 +51,48 @@ fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||
Ok(contents.contains("pub fn main"))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct CargoToml {
|
||||
dependencies: Table,
|
||||
}
|
||||
|
||||
fn is_api_only_mode(base_path: &PathBuf) -> bool {
|
||||
if let Ok(file) = File::open(base_path.join("Cargo.toml")) {
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
if let Ok(_) = buf_reader.read_to_string(&mut contents) {
|
||||
let cargo_toml = toml::from_str::<CargoToml>(&contents).unwrap_or_else(|_| {
|
||||
eprintln!("Failed to parse Cargo.toml content");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
match cargo_toml.dependencies.get("tuono_lib") {
|
||||
Some(tuono_lib) => match tuono_lib {
|
||||
Value::String(_) => return false,
|
||||
Value::Table(elements) => match elements.get("features") {
|
||||
Some(features) => match features {
|
||||
Value::Array(ftrs) => {
|
||||
return ftrs.contains(&Value::String("api".into()))
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
None => return false,
|
||||
},
|
||||
_ => return false,
|
||||
},
|
||||
None => {
|
||||
eprintln!("tuono_lib dependency not found in Cargo.toml");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("Failed to read Cargo.toml file");
|
||||
std::process::exit(1);
|
||||
};
|
||||
eprintln!("Cargo.toml file not found");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let base_path = std::env::current_dir().expect("Failed to read current_dir");
|
||||
@@ -54,7 +100,8 @@ impl App {
|
||||
let mut app = App {
|
||||
route_map: HashMap::new(),
|
||||
base_path: base_path.clone(),
|
||||
has_app_state: has_app_state(base_path).unwrap_or(false),
|
||||
has_app_state: has_app_state(&base_path).unwrap_or(false),
|
||||
is_api_only_mode: is_api_only_mode(&base_path),
|
||||
};
|
||||
|
||||
app.collect_routes();
|
||||
@@ -118,7 +165,7 @@ impl App {
|
||||
|
||||
if entry.extension().expect("failed to read entry extension") == "rs" {
|
||||
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||
let mut route = Route::new(path);
|
||||
let mut route = Route::new(path, self.is_api_only_mode);
|
||||
route.update_axum_info();
|
||||
route_map.insert(route);
|
||||
} else {
|
||||
@@ -128,7 +175,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||
let route = Route::new(path);
|
||||
let route = Route::new(path, self.is_api_only_mode);
|
||||
route_map.insert(route);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,15 +88,17 @@ pub fn app() -> std::io::Result<()> {
|
||||
check_ports(Mode::Dev);
|
||||
|
||||
let app = init_tuono_folder(Mode::Dev)?;
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
if !app.is_api_only_mode {
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
}
|
||||
|
||||
watch::watch().unwrap();
|
||||
watch::watch(app.is_api_only_mode).unwrap();
|
||||
}
|
||||
Actions::Build { ssg, no_js_emit } => {
|
||||
let app = init_tuono_folder(Mode::Prod)?;
|
||||
|
||||
if no_js_emit {
|
||||
if no_js_emit || app.is_api_only_mode {
|
||||
println!("Rust build successfully finished");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ pub struct ApiData {
|
||||
}
|
||||
|
||||
impl ApiData {
|
||||
pub fn new(path: &String) -> Option<Self> {
|
||||
if !path.starts_with("/api/") {
|
||||
pub fn new(path: &String, is_api_only_mode: bool) -> Option<Self> {
|
||||
if !path.starts_with("/api/") && !is_api_only_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -124,12 +124,12 @@ pub struct Route {
|
||||
}
|
||||
|
||||
impl Route {
|
||||
pub fn new(cleaned_path: String) -> Self {
|
||||
pub fn new(cleaned_path: String, is_api_only_mode: bool) -> Self {
|
||||
Route {
|
||||
path: cleaned_path.clone(),
|
||||
axum_info: None,
|
||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path, is_api_only_mode),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,12 +232,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_correctly_create_the_axum_infos() {
|
||||
let info = AxumInfo::new(&Route::new("/index".to_string()));
|
||||
let info = AxumInfo::new(&Route::new("/index".to_string(), false));
|
||||
|
||||
assert_eq!(info.axum_route, "/");
|
||||
assert_eq!(info.module_import, "index");
|
||||
|
||||
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string()));
|
||||
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string(), false));
|
||||
|
||||
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
];
|
||||
|
||||
for (path, html) in routes {
|
||||
let route = Route::new(path.to_string());
|
||||
let route = Route::new(path.to_string(), false);
|
||||
|
||||
assert_eq!(route.output_file_path(), PathBuf::from(html))
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ mod tests {
|
||||
fn should_load_the_axum_get_function() {
|
||||
let mut source_builder = App::new();
|
||||
|
||||
let mut route = Route::new(String::from("index.tsx"));
|
||||
let mut route = Route::new(String::from("index.tsx"), false);
|
||||
route.update_axum_info();
|
||||
|
||||
source_builder
|
||||
|
||||
+30
-22
@@ -52,20 +52,7 @@ fn build_react_ssr_src() -> Job {
|
||||
.0
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn watch() -> Result<()> {
|
||||
watch_react_src().start().await;
|
||||
|
||||
let run_server = build_rust_src();
|
||||
|
||||
let build_ssr_bundle = build_react_ssr_src();
|
||||
|
||||
build_ssr_bundle.start().await;
|
||||
|
||||
run_server.start().await;
|
||||
|
||||
build_ssr_bundle.to_wait().await;
|
||||
|
||||
async fn watch_exec_handler(is_api_only_mode: bool, rust_server: Job, vite_server: Option<Job>) {
|
||||
let wx = Watchexec::new(move |mut action| {
|
||||
let mut should_reload_ssr_bundle = false;
|
||||
let mut should_reload_rust_server = false;
|
||||
@@ -78,7 +65,7 @@ pub async fn watch() -> Result<()> {
|
||||
}
|
||||
|
||||
// Either tsx, jsx and mdx
|
||||
if file_path.ends_with("sx") || file_path.ends_with("mdx") {
|
||||
if !is_api_only_mode && (file_path.ends_with("sx") || file_path.ends_with("mdx")) {
|
||||
should_reload_ssr_bundle = true
|
||||
}
|
||||
}
|
||||
@@ -86,14 +73,15 @@ pub async fn watch() -> Result<()> {
|
||||
|
||||
if should_reload_rust_server {
|
||||
println!(" Reloading...");
|
||||
run_server.stop();
|
||||
rust_server.stop();
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
|
||||
run_server.start();
|
||||
rust_server.start();
|
||||
}
|
||||
|
||||
if should_reload_ssr_bundle {
|
||||
build_ssr_bundle.stop();
|
||||
build_ssr_bundle.start();
|
||||
if should_reload_ssr_bundle && !is_api_only_mode {
|
||||
let vite = vite_server.as_ref().unwrap();
|
||||
vite.stop();
|
||||
vite.start();
|
||||
}
|
||||
|
||||
// if Ctrl-C is received, quit
|
||||
@@ -102,11 +90,31 @@ pub async fn watch() -> Result<()> {
|
||||
}
|
||||
|
||||
action
|
||||
})?;
|
||||
})
|
||||
.expect("Failed to create WatchExec hanlder");
|
||||
|
||||
// watch the current directory
|
||||
wx.config.pathset(["./src"]);
|
||||
|
||||
let _ = wx.main().await.into_diagnostic()?;
|
||||
let _ = wx.main().await.into_diagnostic().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn watch(is_api_only_mode: bool) -> Result<()> {
|
||||
let run_server = build_rust_src();
|
||||
|
||||
if !is_api_only_mode {
|
||||
watch_react_src().start().await;
|
||||
let build_ssr_bundle = build_react_ssr_src();
|
||||
build_ssr_bundle.start().await;
|
||||
run_server.start().await;
|
||||
build_ssr_bundle.to_wait().await;
|
||||
watch_exec_handler(is_api_only_mode, run_server, Some(build_ssr_bundle)).await;
|
||||
} else {
|
||||
run_server.start().await;
|
||||
|
||||
watch_exec_handler(is_api_only_mode, run_server, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]";
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_the_index_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_route("./src/routes/index.rs");
|
||||
|
||||
@@ -36,7 +36,7 @@ fn it_successfully_create_the_index_route() {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_an_api_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_api("./src/routes/api/health_check.rs", POST_API_FILE);
|
||||
|
||||
@@ -65,7 +65,7 @@ fn it_successfully_create_an_api_route() {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_multiple_api_for_the_same_file() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_api(
|
||||
"./src/routes/api/health_check.rs",
|
||||
@@ -98,7 +98,7 @@ fn it_successfully_create_multiple_api_for_the_same_file() {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_catch_all_routes() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_route("./src/routes/[...all_routes].rs");
|
||||
|
||||
@@ -135,3 +135,34 @@ fn it_successfully_create_catch_all_routes() {
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/__tuono/data/*all_routes", get(dyn__catch_all_all_routes::api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_an_api_only_project() {
|
||||
let temp_tuono_project = TempTuonoProject::new(true);
|
||||
|
||||
temp_tuono_project.add_api(
|
||||
"./src/routes/blog/[slug].rs",
|
||||
&format!("{POST_API_FILE}\n{GET_API_FILE}"),
|
||||
);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/blog/[slug].rs"]"#));
|
||||
|
||||
dbg!(&temp_main_rs_content);
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/blog/:slug", post(api_dyn__catch_all_all_apis::post__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
@@ -12,12 +12,24 @@ pub struct TempTuonoProject {
|
||||
}
|
||||
|
||||
impl TempTuonoProject {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(is_api_only: bool) -> Self {
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
let mut file = File::create(temp_dir.path().join("Cargo.toml"))
|
||||
.expect("Failed to create Cargo.toml file");
|
||||
if is_api_only {
|
||||
file.write_all(
|
||||
b"[dependencies]\ntuono_lib = { version = \"0.1.0\", features = [\"api\"] }",
|
||||
)
|
||||
.expect("Failed to write into Cargo.toml file");
|
||||
} else {
|
||||
file.write_all(b"[dependencies]\ntuono_lib = \"0.1.0\"")
|
||||
.expect("Failed to write into Cargo.toml file");
|
||||
}
|
||||
|
||||
TempTuonoProject {
|
||||
original_dir,
|
||||
temp_dir,
|
||||
|
||||
+30
-16
@@ -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"]}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.tuono
|
||||
target
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "tuono-api"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = { path = "../../crates/tuono_lib/", features = ["api"]}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Tuono API started
|
||||
|
||||
This is the starter tuono project. To download it run in your terminal:
|
||||
|
||||
```shell
|
||||
$ tuono new [NAME] --template tuono-api
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
use tuono_lib::axum::http::StatusCode;
|
||||
use tuono_lib::Request;
|
||||
|
||||
#[tuono_lib::api(GET)]
|
||||
pub async fn root_route_api(_req: Request) -> &'static str {
|
||||
"get request"
|
||||
}
|
||||
|
||||
#[tuono_lib::api(POST)]
|
||||
pub async fn root_route_api_post(_req: Request) -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
Reference in New Issue
Block a user