tovuk 0.1.68

Deploy Rust workers, static frontends, and worker-static apps to Tovuk.
use super::{
    super::{
        constants::PROJECT_TEMPLATES,
        errors::{Result, agent_error, internal_error},
        project::service_name_from_dir,
        template_sources::{
            frontend_package_json, frontend_source, frontend_ts_config, frontend_vite_env_source,
            rust_api_source, rust_template_cargo_lock, rust_template_cargo_toml,
        },
    },
    config::{frontend_config, fullstack_config, rust_backend_config},
};
use std::{fs, path::Path};

pub(super) fn create_template(project_dir: &Path, template: &str) -> Result<()> {
    if !PROJECT_TEMPLATES.contains(&template) {
        return Err(agent_error(
            "invalid_template",
            "Tovuk template is unknown.",
            format!("Use one of: {}.", PROJECT_TEMPLATES.join(", ")),
            false,
        ));
    }
    match template {
        "rust-worker" => {
            write_rust_api_template(project_dir, &service_name_from_dir(project_dir), true)?;
        }
        "tanstack-static-frontend" => {
            write_frontend_template(
                project_dir,
                &service_name_from_dir(project_dir),
                "/api",
                true,
            )?;
        }
        "worker-static-rust-tanstack" => write_fullstack_template(project_dir)?,
        _ => {}
    }
    println!("created {template} template");
    Ok(())
}

fn write_fullstack_template(project_dir: &Path) -> Result<()> {
    write_rust_api_template(&project_dir.join("api"), "api", false)?;
    write_frontend_template(&project_dir.join("web"), "web", "/api", false)?;
    write_new_file(
        &project_dir.join("tovuk.toml"),
        &fullstack_config(project_dir, "api", "web", true),
    )
}

fn write_rust_api_template(project_dir: &Path, name: &str, include_config: bool) -> Result<()> {
    fs::create_dir_all(project_dir.join("src"))
        .map_err(|error| internal_error(error.to_string()))?;
    write_new_file(
        &project_dir.join("Cargo.toml"),
        &rust_template_cargo_toml(name),
    )?;
    write_new_file(
        &project_dir.join("Cargo.lock"),
        &rust_template_cargo_lock(name),
    )?;
    write_new_file(&project_dir.join("src/main.rs"), rust_api_source())?;
    if include_config {
        write_new_file(
            &project_dir.join("tovuk.toml"),
            &rust_backend_config(project_dir),
        )?;
    }
    Ok(())
}

fn write_frontend_template(
    project_dir: &Path,
    name: &str,
    api_base_url: &str,
    include_config: bool,
) -> Result<()> {
    fs::create_dir_all(project_dir.join("src"))
        .map_err(|error| internal_error(error.to_string()))?;
    write_new_file(
        &project_dir.join("package.json"),
        &frontend_package_json(name),
    )?;
    write_new_file(
        &project_dir.join("index.html"),
        "<div id=\"root\"></div><script type=\"module\" src=\"/src/main.tsx\"></script>\n",
    )?;
    write_new_file(
        &project_dir.join("src/styles.css"),
        "body{margin:0;font-family:system-ui,sans-serif}main{min-height:100svh;display:grid;place-items:center;padding:2rem}code{font-family:ui-monospace,monospace}\n",
    )?;
    write_new_file(
        &project_dir.join("src/vite-env.d.ts"),
        frontend_vite_env_source(),
    )?;
    write_new_file(
        &project_dir.join("src/main.tsx"),
        &frontend_source(api_base_url),
    )?;
    write_new_file(&project_dir.join("tsconfig.json"), &frontend_ts_config())?;
    write_new_file(
        &project_dir.join("vite.config.ts"),
        "import react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({ plugins: [react()] });\n",
    )?;
    if include_config {
        write_new_file(
            &project_dir.join("tovuk.toml"),
            &frontend_config(project_dir, true),
        )?;
    }
    println!(
        "run package install in the frontend directory before doctor: bun install or npm install"
    );
    Ok(())
}

fn write_new_file(path: &Path, source: &str) -> Result<()> {
    if path.exists() {
        return Err(agent_error(
            "file_exists",
            format!("Refusing to overwrite {}.", path.display()),
            "Move the existing file or choose an empty directory, then retry.",
            false,
        ));
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| internal_error(error.to_string()))?;
    }
    fs::write(path, source).map_err(|error| internal_error(error.to_string()))
}