nfw-core 0.1.1

Blazing fast fullstack framework powered by NestForge
Documentation
use axum::{
    extract::State,
    response::{Html, IntoResponse, Response},
    routing::get,
    Router,
};

use crate::config::NestForgeWebConfig;
use crate::routing::RouteScanner;
use crate::server::renderer::{PageProps, Renderer};

#[derive(Clone)]
pub struct AppState {
    pub config: NestForgeWebConfig,
    pub routes: Vec<crate::routing::Route>,
    pub renderer: Renderer,
}

pub struct NestForgeWebApp {
    config: NestForgeWebConfig,
}

impl NestForgeWebApp {
    pub fn new(config: NestForgeWebConfig) -> Self {
        Self { config }
    }

    pub async fn build(&self) -> anyhow::Result<Router> {
        let scanner = RouteScanner::new(&self.config.app_dir);
        let routes = scanner.scan().await?;
        let renderer = Renderer::new(std::path::PathBuf::from(&self.config.app_dir));

        let state = AppState {
            config: self.config.clone(),
            routes,
            renderer,
        };

        let app = Router::new()
            .route("/health", get(health_handler))
            .route("/", get(root_handler))
            .with_state(state);

        Ok(app)
    }

    pub async fn listen(&self) -> anyhow::Result<()> {
        let app = self.build().await?;
        let addr = format!("{}:{}", self.config.host, self.config.port);

        tracing::info!("Starting NestForge Web server on {}", addr);
        tracing::info!("Server ready at http://{}", addr);

        let listener = tokio::net::TcpListener::bind(&addr).await?;
        axum::serve(listener, app).await?;

        Ok(())
    }
}

async fn health_handler() -> &'static str {
    "healthy"
}

async fn root_handler(State(state): State<AppState>) -> Response {
    let page_props = PageProps::default();

    match state.renderer.get_layout_for_path("/") {
        Some(layout) => {
            match state
                .renderer
                .render_with_layout("/page.tsx", page_props, &layout)
                .await
            {
                Ok(html) => Html(html).into_response(),
                Err(_) => Html(get_default_html("Welcome to NestForge Web")).into_response(),
            }
        }
        None => {
            let default_page = format!("{}/page.tsx", state.config.app_dir);
            match state.renderer.render(&default_page, page_props).await {
                Ok(html) => Html(html).into_response(),
                Err(_) => Html(get_default_html("Welcome to NestForge Web")).into_response(),
            }
        }
    }
}

fn get_default_html(title: &str) -> String {
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{}</title>
</head>
<body>
    <main>
        <h1>{}</h1>
        <p>Start building your fullstack application</p>
    </main>
</body>
</html>"#,
        title, title
    )
}

pub async fn start_dev_server(config: NestForgeWebConfig) -> anyhow::Result<()> {
    let app = NestForgeWebApp::new(config);
    app.listen().await
}

pub async fn build_for_production(app_dir: &str) -> anyhow::Result<()> {
    tracing::info!("Building NestForge Web application...");

    let dist_dir = format!("{}/.next", app_dir);
    std::fs::create_dir_all(&dist_dir)?;

    tracing::info!("Build complete. Output: {}", dist_dir);
    Ok(())
}