nfw-core 0.1.1

Blazing fast fullstack framework powered by NestForge
Documentation
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;

use crate::server::renderer::PageProps;

#[derive(Debug, Clone)]
pub struct Renderer {
    pages_dir: PathBuf,
    build_dir: PathBuf,
    node_modules: PathBuf,
}

impl Renderer {
    pub fn new(pages_dir: PathBuf) -> Self {
        let build_dir = pages_dir.join(".next");
        let node_modules = pages_dir.parent().unwrap_or(&pages_dir).join("node_modules");
        
        Self {
            pages_dir,
            build_dir,
            node_modules,
        }
    }

    pub async fn render(&self, path: &str, props: PageProps) -> anyhow::Result<String> {
        let script = format!(
            r#"
import React from 'react';
import ReactDOMServer from 'react-dom/server';

async function renderPage() {{
    const {{ default: Page }} = await import('./{}');
    const props = {};
    
    const html = ReactDOMServer.renderToString(
        React.createElement(Page, {{ params: props.params, searchParams: props.searchParams }})
    );
    
    console.log(html);
}}

renderPage().catch(console.error);
"#,
            path.replace("\\", "/"),
            serde_json::to_string(&props)?
        );
        
        let mut child = Command::new("node")
            .args(["--input-type=module", "-e", &script])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .current_dir(self.pages_dir.parent().unwrap_or(&self.pages_dir))
            .spawn()?;
        
        let output = child.wait_with_output().await?;
        
        if output.status.success() {
            let html = String::from_utf8_lossy(&output.stdout).to_string();
            Ok(html.trim().to_string())
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("Render failed: {}", stderr);
        }
    }

    pub async fn render_with_layout(&self, path: &str, props: PageProps, layout: &str) -> anyhow::Result<String> {
        let layout_html = self.render(layout, props.clone()).await?;
        let page_html = self.render(path, props).await?;
        
        let html = layout_html.replace("{{children}}", &page_html);
        Ok(html)
    }

    pub fn get_layout_for_path(&self, path: &str) -> Option<String> {
        let mut current = PathBuf::from(&self.pages_dir);
        
        for segment in path.trim_start_matches('/').split('/') {
            if segment.is_empty() {
                continue;
            }
            current.push(segment);
        }
        
        loop {
            let layout_path = current.join("layout.tsx");
            if layout_path.exists() {
                return Some(layout_path.to_string_lossy().to_string());
            }
            
            if !current.pop() {
                break;
            }
        }
        
        let root_layout = self.pages_dir.join("layout.tsx");
        if root_layout.exists() {
            Some(root_layout.to_string_lossy().to_string())
        } else {
            None
        }
    }

    pub fn path_to_page(&self, route_path: &str) -> String {
        let mut page_path = self.pages_dir.clone();
        
        for segment in route_path.trim_start_matches('/').split('/') {
            if segment.is_empty() {
                continue;
            }
            page_path.push(segment);
        }
        
        let tsx_page = page_path.with_file_name(format!("{}.tsx", page_path.file_name().unwrap_or_default()));
        let ts_page = page_path.with_file_name(format!("{}.ts", page_path.file_name().unwrap_or_default()));
        
        if tsx_page.exists() {
            tsx_page.to_string_lossy().to_string()
        } else if ts_page.exists() {
            ts_page.to_string_lossy().to_string()
        } else {
            tsx_page.to_string_lossy().to_string()
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct RenderOptions {
    pub streaming: bool,
    pub cache_ttl: Option<u64>,
    pub revalidate: bool,
}

pub struct StreamingRenderer {
    renderer: Renderer,
}

impl StreamingRenderer {
    pub fn new(renderer: Renderer) -> Self {
        Self { renderer }
    }

    pub async fn stream(&self, path: &str, props: PageProps) -> anyhow::Result<tokio::io::Lines<tokio::io::BufReader<tokio::process::ChildStdout>>> {
        let page = self.renderer.path_to_page(path);
        let script = format!(
            r#"
import React from 'react';
import {{ renderToPipeableStream }} from 'react-dom/server';
import {{ default: Page }} = await import('./{}');
const props = {};
const stream = renderToPipeableStream(
    React.createElement(Page, {{ params: props.params, searchParams: props.searchParams }}),
    {{ onShellReady() {{ }}, onAllReady() {{ }} }}
);
"#,
            page.replace("\\", "/")
        );
        
        let mut child = Command::new("node")
            .args(["--input-type=module", "-e", &script])
            .stdout(Stdio::piped())
            .current_dir(self.renderer.pages_dir.parent().unwrap_or(&self.renderer.pages_dir))
            .spawn()?;
        
        let stdout = child.stdout.take().unwrap();
        Ok(tokio::io::BufReader::new(stdout).lines())
    }
}