forte-cli 0.3.13

CLI for the Forte fullstack web framework
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use uuid::Uuid;

pub fn generate_vite_server_script(socket_path: &Path) -> String {
    let socket_path_str = socket_path.display();
    format!(
        r#"import {{ createServer }} from "vite";
import {{ createServer as createHttpServer }} from "http";
import {{ unlinkSync, existsSync }} from "fs";

const SOCKET_PATH = "{socket_path_str}";

function cleanup() {{
    try {{
        if (existsSync(SOCKET_PATH)) {{
            unlinkSync(SOCKET_PATH);
        }}
    }} catch (e) {{
        // ignore
    }}
}}

process.on("exit", cleanup);
process.on("SIGINT", () => {{ cleanup(); process.exit(0); }});
process.on("SIGTERM", () => {{ cleanup(); process.exit(0); }});

process.stdin.resume();
process.stdin.on("end", () => {{
    cleanup();
    process.exit(0);
}});
process.stdin.on("close", () => {{
    cleanup();
    process.exit(0);
}});

async function streamToString(stream) {{
    const reader = stream.getReader();
    const chunks = [];
    while (true) {{
        const {{ done, value }} = await reader.read();
        if (done) break;
        chunks.push(value);
    }}
    const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
    const result = new Uint8Array(totalLength);
    let offset = 0;
    for (const chunk of chunks) {{
        result.set(chunk, offset);
        offset += chunk.length;
    }}
    return result;
}}

async function main() {{
    cleanup();

    const vite = await createServer({{
        server: {{
            middlewareMode: true,
        }},
        appType: "custom",
    }});


    const server = createHttpServer(async (req, res) => {{
        if (req.method === "POST" && req.url === "/__ssr_render") {{
            let body = "";
            req.on("data", (chunk) => {{
                body += chunk;
            }});
            req.on("end", async () => {{
                try {{
                    const {{ url, props, cookie }} = JSON.parse(body);
                    const {{ renderStream }} = await vite.ssrLoadModule("/src/server.tsx");
                    const {{ stream, cookies }} = await renderStream(url, props, cookie);

                    const htmlBuffer = Buffer.from(await streamToString(stream));
                    res.useChunkedEncodingByDefault = false;
                    res.setHeader("Content-Type", "text/html");
                    res.setHeader("Content-Length", htmlBuffer.length);
                    if (cookies && cookies.length > 0) {{
                        res.setHeader("Set-Cookie", cookies);
                    }}
                    res.writeHead(200);
                    res.end(htmlBuffer);
                }} catch (e) {{
                    vite.ssrFixStacktrace(e);
                    console.error("[vite-ssr] Error:", e.stack || e.message);
                    res.writeHead(500, {{ "Content-Type": "text/plain" }});
                    res.end(e.stack || e.message);
                }}
            }});
            return;
        }}

        vite.middlewares(req, res);
    }});

    server.listen(SOCKET_PATH);
}}

main().catch((e) => {{
    console.error("[vite-ssr] Failed to start:", e);
    process.exit(1);
}});
"#
    )
}

pub struct Vite {
    pub child: Child,
    pub socket_path: PathBuf,
}

pub fn spawn_vite(fe_dir: &Path, forte_port: u16) -> Result<Vite> {
    let dev_dir = fe_dir.join(".forte/dev");
    std::fs::create_dir_all(&dev_dir)?;

    let session_id = Uuid::new_v4();
    let socket_path = dev_dir.join(format!("vite-{}.sock", session_id));

    let script_path = dev_dir.join(format!("vite-ssr-server-{}.mjs", session_id));
    let script_content = generate_vite_server_script(&socket_path);
    std::fs::write(&script_path, &script_content)?;

    let child = Command::new("node")
        .arg(&script_path)
        .current_dir(fe_dir)
        .env("FORTE_PORT", forte_port.to_string())
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .spawn()?;

    Ok(Vite { child, socket_path })
}

pub async fn wait_for_vite_ready(socket_path: &Path) -> Result<()> {
    for _ in 0..50 {
        if socket_path.exists() {
            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
            return Ok(());
        }
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    }

    anyhow::bail!("Vite server did not start within 5 seconds")
}