feat: update source builder

This commit is contained in:
Valerio Ageno
2024-05-29 21:31:28 +02:00
parent c12739d526
commit 1c0b4bba04
3 changed files with 245 additions and 130 deletions
+180 -124
View File
@@ -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<String> {
let pathname = &request.uri();
let headers = &request.headers();
struct SourceBuilder {
/// Key is the path
route_map: HashMap<PathBuf, Route>,
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<PathBuf, GlobError>) {
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<String, String> {
let mut mods_map: HashMap<String, String> = 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, String>) -> String {
fn create_routes_declaration(routes: &HashMap<PathBuf, Route>) -> 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, String>) -> String {
fn create_modules_declaration(routes: &HashMap<PathBuf, Route>) -> 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)
)
})
}
}
@@ -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<String> {
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()),
}
}
"##;
+3 -6
View File
@@ -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(),
}));