diff --git a/crates/tuono/src/source_builder.rs b/crates/tuono/src/source_builder.rs index c32efebc..6f79f1b6 100644 --- a/crates/tuono/src/source_builder.rs +++ b/crates/tuono/src/source_builder.rs @@ -1,4 +1,5 @@ use glob::glob; +use glob::GlobError; use std::collections::HashMap; use std::fs; use std::io; @@ -6,157 +7,130 @@ use std::io::prelude::*; use std::path::Path; use std::path::PathBuf; -const ROOT_FOLDER: &'static str = "src/routes"; -const DEV_FOLDER: &'static str = ".tuono"; -const SERVER_ENTRY_DATA: &'static str = "// File automatically generated by tuono +mod static_files; -import { routeTree } from './routeTree.gen' -import { serverSideRendering } from 'tuono/ssr' - -export const renderFn = serverSideRendering(routeTree) -"; - -const CLIENT_ENTRY_DATA: &'static str = "// File automatically generated by tuono - -import { hydrate } from 'tuono/hydration' - -// Import the generated route tree -import { routeTree } from './routeTree.gen' - -hydrate(routeTree) -"; - -enum Mode { +const ROOT_FOLDER: &str = "src/routes"; +const DEV_FOLDER: &str = ".tuono"; +pub enum Mode { Prod, Dev, } -const AXUM_ENTRY_POINT: &'static str = r##" -// File automatically generated -// Do not manually change it - -use axum::extract::Request; -use axum::response::Html; -use axum::{routing::get, Router}; -use tower_http::services::ServeDir; -use tuono_lib::{ssr, Ssr}; - -// MODULE_IMPORTS - -#[tokio::main] -async fn main() { - Ssr::create_platform(); - - let app = Router::new() - // ROUTE_BUILDER - .fallback_service(ServeDir::new("public").fallback(get(catch_all))); - - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - axum::serve(listener, app).await.unwrap(); +#[derive(Debug, PartialEq, Eq)] +struct Route { + /// Every module import is the path with a _ instead of / + pub module_import: String, + pub axum_route: String, } -async fn catch_all(request: Request) -> Html { - let pathname = &request.uri(); - let headers = &request.headers(); +struct SourceBuilder { + /// Key is the path + route_map: HashMap, + mode: Mode, + base_path: PathBuf, +} - let req = tuono_lib::Request::new(pathname, headers); +impl SourceBuilder { + pub fn new(mode: Mode) -> Self { + let base_path = std::env::current_dir().unwrap(); + SourceBuilder { + route_map: HashMap::new(), + mode, + base_path, + } + } + fn collect_routes(&mut self) { + glob(self.base_path.join("src/routes/**/*.rs").to_str().unwrap()) + .unwrap() + .for_each(|entry| self.collect_route(entry)) + } - // TODO: remove unwrap - let payload = tuono_lib::Payload::new(&req, Box::new("")) - .client_payload() - .unwrap(); + fn collect_route(&mut self, path_buf: Result) { + let entry = path_buf.unwrap(); - let result = ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload))); + let route_name = self.get_route_name(&entry); - match result { - Ok(html) => Html(html), - _ => Html("500 internal server error".to_string()), + // Remove first slash + let mut module = route_name.as_str().chars(); + module.next(); + + let base_path_str = self.base_path.to_str().unwrap(); + + let path = entry + .to_str() + .unwrap() + .replace(&format!("{base_path_str}/src/routes"), ""); + + let axum_route = path.clone().replace("/index.rs", "").replace(".rs", ""); + + // Is root / + if axum_route.is_empty() { + let route = Route { + module_import: module.as_str().to_string().replace('/', "_"), + axum_route: "/".to_string(), + }; + + self.route_map.insert(PathBuf::from(path), route); + return; + } + + let route = Route { + module_import: module.as_str().to_string().replace('/', "_"), + axum_route, + }; + + self.route_map.insert(PathBuf::from(path), route); + } + + fn get_route_name(&self, path: &Path) -> String { + let base_path_str = self.base_path.to_str().unwrap(); + + path.to_str() + .unwrap() + .replace(&format!("{base_path_str}/src/routes"), "") + .replace(".rs", "") } } -"##; fn create_main_file(base_path: &Path, bundled_file: &String) { let mut data_file = fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed"); data_file - .write(bundled_file.as_bytes()) + .write_all(bundled_file.as_bytes()) .expect("write failed"); } -fn get_route_name<'a>(path: &PathBuf, base_path: &Path) -> String { - let base_path_str = base_path.to_str().unwrap(); - if base_path_str == "." { - return path.to_str().unwrap().replace("/src/routes/", ""); - } - - // TODO: refactor this horrible code - if base_path_str.ends_with("/") { - return path - .to_str() - .unwrap() - .replace(&format!("{base_path_str}src/routes"), "") - .replace(".rs", "") - .replace("index", ""); - } - - path.to_str() - .unwrap() - .replace(&format!("{base_path_str}/src/routes"), "") - .replace(".rs", "") - .replace("index", "") -} - -fn collect_handlers(base_path: &Path) -> HashMap { - let mut mods_map: HashMap = HashMap::new(); - - let _: Vec<_> = glob(base_path.join("src/routes/**/*.rs").to_str().unwrap()) - .unwrap() - .map(|entry| { - let entry = entry.unwrap(); - - let route_name = get_route_name(&entry, base_path); - - // Remove first slash - let mut module = route_name.as_str().chars(); - module.next(); - - mods_map.insert( - module.as_str().to_string(), - entry - .to_str() - .unwrap() - .replace(base_path.to_str().unwrap(), ""), - ); - }) - .collect(); - - dbg!(&mods_map); - - return mods_map; -} - -fn create_axum_routes_declaration(routes: &HashMap) -> String { +fn create_routes_declaration(routes: &HashMap) -> String { let mut route_declarations = String::from("// ROUTE_BUILDER\n"); - for (route, _path) in routes.iter() { - route_declarations.push_str(&format!(r#".route("/{route}", get({route}::route))"#)); + for (_, route) in routes.iter() { + let Route { + axum_route, + module_import, + } = &route; + route_declarations.push_str(&format!( - r#".route("/__tuono/data/{route}", get({route}::api))"# + r#".route("{axum_route}", get({module_import}::route))"# + )); + route_declarations.push_str(&format!( + r#".route("/__tuono/data{axum_route}", get({module_import}::api))"# )); } route_declarations } -fn create_modules_declaration(routes: &HashMap) -> String { +fn create_modules_declaration(routes: &HashMap) -> String { let mut route_declarations = String::from("// MODULE_IMPORTS\n"); - for (route, path) in routes.iter() { + for (path, route) in routes.iter() { + let module_name = &route.module_import; + let path_str = path.to_str().unwrap(); route_declarations.push_str(&format!( - r#"#[path="..{path}"] -mod {route}; + r#"#[path="../{ROOT_FOLDER}{path_str}"] +mod {module_name}; "# )) } @@ -169,11 +143,19 @@ pub fn bundle_axum_source() -> io::Result<()> { let base_path = std::env::current_dir().unwrap(); - let mods = collect_handlers(&base_path); + let mut source_builder = SourceBuilder::new(Mode::Dev); - let bundled_file = AXUM_ENTRY_POINT - .replace("// ROUTE_BUILDER\n", &create_axum_routes_declaration(&mods)) - .replace("// MODULE_IMPORTS\n", &create_modules_declaration(&mods)); + source_builder.collect_routes(); + + let bundled_file = static_files::AXUM_ENTRY_POINT + .replace( + "// ROUTE_BUILDER\n", + &create_routes_declaration(&source_builder.route_map), + ) + .replace( + "// MODULE_IMPORTS\n", + &create_modules_declaration(&source_builder.route_map), + ); create_main_file(&base_path, &bundled_file); @@ -193,11 +175,85 @@ pub fn check_tuono_folder() -> io::Result<()> { pub fn create_client_entry_files() -> io::Result<()> { let dev_folder = Path::new(DEV_FOLDER); - let mut server_entry = fs::File::create(&dev_folder.join("server-main.tsx"))?; - let mut client_entry = fs::File::create(&dev_folder.join("client-main.tsx"))?; + let mut server_entry = fs::File::create(dev_folder.join("server-main.tsx"))?; + let mut client_entry = fs::File::create(dev_folder.join("client-main.tsx"))?; - server_entry.write(&SERVER_ENTRY_DATA.as_bytes())?; - client_entry.write(&CLIENT_ENTRY_DATA.as_bytes())?; + server_entry.write_all(static_files::SERVER_ENTRY_DATA.as_bytes())?; + client_entry.write_all(static_files::CLIENT_ENTRY_DATA.as_bytes())?; Ok(()) } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn collect_routes() { + let mut source_builder = SourceBuilder::new(Mode::Dev); + source_builder.base_path = "/home/user/Documents/tuono".into(); + + let routes = [ + "/home/user/Documents/tuono/src/routes/about.rs", + "/home/user/Documents/tuono/src/routes/index.rs", + "/home/user/Documents/tuono/src/routes/posts/index.rs", + ]; + + routes + .into_iter() + .for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route)))); + + let results = [ + ("/index.rs", "index"), + ("/about.rs", "about"), + ("/posts/index.rs", "posts_index"), + ]; + + results.into_iter().for_each(|(path, module_import)| { + assert_eq!( + source_builder + .route_map + .get(&PathBuf::from(path)) + .unwrap() + .module_import, + String::from(module_import) + ) + }) + } + + #[test] + fn create_multi_level_axum_paths() { + let mut source_builder = SourceBuilder::new(Mode::Dev); + source_builder.base_path = "/home/user/Documents/tuono".into(); + + let routes = [ + "/home/user/Documents/tuono/src/routes/about.rs", + "/home/user/Documents/tuono/src/routes/index.rs", + "/home/user/Documents/tuono/src/routes/posts/index.rs", + "/home/user/Documents/tuono/src/routes/posts/any-post.rs", + ]; + + routes + .into_iter() + .for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route)))); + + let results = [ + ("/index.rs", "/"), + ("/about.rs", "/about"), + ("/posts/index.rs", "/posts"), + ("/posts/any-post.rs", "/posts/any-post"), + ]; + + results.into_iter().for_each(|(path, expected_path)| { + assert_eq!( + source_builder + .route_map + .get(&PathBuf::from(path)) + .unwrap() + .axum_route, + String::from(expected_path) + ) + }) + } +} diff --git a/crates/tuono/src/source_builder/static_files.rs b/crates/tuono/src/source_builder/static_files.rs new file mode 100644 index 00000000..1f13256b --- /dev/null +++ b/crates/tuono/src/source_builder/static_files.rs @@ -0,0 +1,62 @@ +pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono + +import { routeTree } from './routeTree.gen' +import { serverSideRendering } from 'tuono/ssr' + +export const renderFn = serverSideRendering(routeTree) +"; + +pub const CLIENT_ENTRY_DATA: &str = "// File automatically generated by tuono + +import { hydrate } from 'tuono/hydration' + +// Import the generated route tree +import { routeTree } from './routeTree.gen' + +hydrate(routeTree) +"; + +pub const AXUM_ENTRY_POINT: &str = r##" +// File automatically generated +// Do not manually change it + +use axum::extract::Request; +use axum::response::Html; +use axum::{routing::get, Router}; +use tower_http::services::ServeDir; +use tuono_lib::{ssr, Ssr}; + +// MODULE_IMPORTS + +#[tokio::main] +async fn main() { + Ssr::create_platform(); + + let app = Router::new() + // ROUTE_BUILDER + .fallback_service(ServeDir::new("public").fallback(get(catch_all))); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn catch_all(request: Request) -> Html { + let pathname = &request.uri(); + let headers = &request.headers(); + + let req = tuono_lib::Request::new(pathname, headers); + + + // TODO: remove unwrap + let payload = tuono_lib::Payload::new(&req, Box::new("")) + .client_payload() + .unwrap(); + + let result = ssr::Js::SSR.with(|ssr| ssr.borrow_mut().render_to_string(Some(&payload))); + + match result { + Ok(html) => Html(html), + _ => Html("500 internal server error".to_string()), + } +} +"##; diff --git a/crates/tuono/src/watch.rs b/crates/tuono/src/watch.rs index 7206ca72..23c63ccd 100644 --- a/crates/tuono/src/watch.rs +++ b/crates/tuono/src/watch.rs @@ -26,8 +26,7 @@ pub async fn watch() -> Result<()> { program: Program::Exec { prog: "node_modules/.bin/tuono-dev-watch".into(), args: vec![], - } - .into(), + }, options: Default::default(), })); @@ -37,8 +36,7 @@ pub async fn watch() -> Result<()> { program: Program::Exec { prog: "cargo".into(), args: vec!["run".to_string()], - } - .into(), + }, options: Default::default(), })); @@ -46,8 +44,7 @@ pub async fn watch() -> Result<()> { program: Program::Exec { prog: "node_modules/.bin/tuono-dev-ssr".into(), args: vec![], - } - .into(), + }, options: Default::default(), }));