mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: update source builder
This commit is contained in:
+180
-124
@@ -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()),
|
||||
}
|
||||
}
|
||||
"##;
|
||||
@@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user