Compare commits

...

3 Commits

Author SHA1 Message Date
Valerio Ageno 12a2144a0d feat: add tuono-api example 2024-12-14 12:15:13 +01:00
Valerio Ageno adf7ba5391 feat: add CLI support to api-only mode 2024-12-14 12:15:13 +01:00
Valerio Ageno edb21ab969 feat: add feature flat to tuono_lib 2024-12-14 12:15:12 +01:00
23 changed files with 498 additions and 240 deletions
+1
View File
@@ -9,6 +9,7 @@ exclude = [
"apps/documentation",
"examples/with-mdx",
"examples/tuono-app",
"examples/tuono-api",
"examples/tuono-tutorial",
"benches/tuono"
]
+1
View File
@@ -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
View File
@@ -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);
}
}
+6 -4
View File
@@ -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(());
}
+7 -7
View File
@@ -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))
}
+1 -1
View File
@@ -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
View File
@@ -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(())
}
+35 -4
View File
@@ -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))"#
));
}
+13 -1
View File
@@ -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
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()
)
}
}
+2
View File
@@ -0,0 +1,2 @@
.tuono
target
+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)
+12
View File
@@ -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"]}
+7
View File
@@ -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
```
View File
+12
View File
@@ -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
}