Skip to main content

nfw_core/server/app/
router.rs

1use axum::{
2    extract::State,
3    response::{Html, IntoResponse, Response},
4    routing::get,
5    Router,
6};
7
8use crate::config::NestForgeWebConfig;
9use crate::routing::RouteScanner;
10use crate::server::renderer::{PageProps, Renderer};
11
12#[derive(Clone)]
13pub struct AppState {
14    pub config: NestForgeWebConfig,
15    pub routes: Vec<crate::routing::Route>,
16    pub renderer: Renderer,
17}
18
19pub struct NestForgeWebApp {
20    config: NestForgeWebConfig,
21}
22
23impl NestForgeWebApp {
24    pub fn new(config: NestForgeWebConfig) -> Self {
25        Self { config }
26    }
27
28    pub async fn build(&self) -> anyhow::Result<Router> {
29        let scanner = RouteScanner::new(&self.config.app_dir);
30        let routes = scanner.scan().await?;
31        let renderer = Renderer::new(std::path::PathBuf::from(&self.config.app_dir));
32
33        let state = AppState {
34            config: self.config.clone(),
35            routes,
36            renderer,
37        };
38
39        let app = Router::new()
40            .route("/health", get(health_handler))
41            .route("/", get(root_handler))
42            .with_state(state);
43
44        Ok(app)
45    }
46
47    pub async fn listen(&self) -> anyhow::Result<()> {
48        let app = self.build().await?;
49        let addr = format!("{}:{}", self.config.host, self.config.port);
50
51        tracing::info!("Starting NestForge Web server on {}", addr);
52        tracing::info!("Server ready at http://{}", addr);
53
54        let listener = tokio::net::TcpListener::bind(&addr).await?;
55        axum::serve(listener, app).await?;
56
57        Ok(())
58    }
59}
60
61async fn health_handler() -> &'static str {
62    "healthy"
63}
64
65async fn root_handler(State(state): State<AppState>) -> Response {
66    let page_props = PageProps::default();
67
68    match state.renderer.get_layout_for_path("/") {
69        Some(layout) => {
70            match state
71                .renderer
72                .render_with_layout("/page.tsx", page_props, &layout)
73                .await
74            {
75                Ok(html) => Html(html).into_response(),
76                Err(_) => Html(get_default_html("Welcome to NestForge Web")).into_response(),
77            }
78        }
79        None => {
80            let default_page = format!("{}/page.tsx", state.config.app_dir);
81            match state.renderer.render(&default_page, page_props).await {
82                Ok(html) => Html(html).into_response(),
83                Err(_) => Html(get_default_html("Welcome to NestForge Web")).into_response(),
84            }
85        }
86    }
87}
88
89fn get_default_html(title: &str) -> String {
90    format!(
91        r#"<!DOCTYPE html>
92<html lang="en">
93<head>
94    <meta charset="UTF-8">
95    <meta name="viewport" content="width=device-width, initial-scale=1.0">
96    <title>{}</title>
97</head>
98<body>
99    <main>
100        <h1>{}</h1>
101        <p>Start building your fullstack application</p>
102    </main>
103</body>
104</html>"#,
105        title, title
106    )
107}
108
109pub async fn start_dev_server(config: NestForgeWebConfig) -> anyhow::Result<()> {
110    let app = NestForgeWebApp::new(config);
111    app.listen().await
112}
113
114pub async fn build_for_production(app_dir: &str) -> anyhow::Result<()> {
115    tracing::info!("Building NestForge Web application...");
116
117    let dist_dir = format!("{}/.next", app_dir);
118    std::fs::create_dir_all(&dist_dir)?;
119
120    tracing::info!("Build complete. Output: {}", dist_dir);
121    Ok(())
122}