nfw_core/server/app/
router.rs1use 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}