forgex 0.9.0

CLI and runtime for the Forge full-stack framework
use anyhow::Result;
use std::fmt;
use std::path::Path;
use std::process::Command;
use std::str::FromStr;

use super::frontend_codegen::{
    BindingGeneratorFn, BindingGeneratorInput, generate_dioxus_bindings, generate_svelte_bindings,
};

type DetectFn = fn(&Path) -> bool;
type PostGenerateFn = fn(&Path) -> Result<()>;
type ExtraFormatFn = fn(&Path) -> Result<bool>;

pub struct FrontendTargetSpec {
    pub id: &'static str,
    pub display_name: &'static str,
    pub default_output_dir: &'static str,
    detect: DetectFn,
    post_generate: PostGenerateFn,
    extra_format: ExtraFormatFn,
    generate_bindings: BindingGeneratorFn,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrontendTarget {
    SvelteKit,
    Dioxus,
}

const SUPPORTED_FRONTENDS: [FrontendTarget; 2] =
    [FrontendTarget::Dioxus, FrontendTarget::SvelteKit];

const SVELTEKIT_SPEC: FrontendTargetSpec = FrontendTargetSpec {
    id: "sveltekit",
    display_name: "SvelteKit",
    default_output_dir: "frontend/src/lib/forge",
    detect: detect_sveltekit,
    post_generate: post_generate_sveltekit,
    extra_format: no_extra_format,
    generate_bindings: generate_svelte_bindings,
};

const DIOXUS_SPEC: FrontendTargetSpec = FrontendTargetSpec {
    id: "dioxus",
    display_name: "Dioxus",
    default_output_dir: "frontend/src/forge",
    detect: detect_dioxus,
    post_generate: no_post_generate,
    extra_format: format_dioxus_frontend,
    generate_bindings: generate_dioxus_bindings,
};

impl FrontendTarget {
    pub fn detect(frontend_dir: &Path) -> Option<Self> {
        SUPPORTED_FRONTENDS
            .into_iter()
            .find(|target| (target.spec().detect)(frontend_dir))
    }

    pub fn spec(self) -> &'static FrontendTargetSpec {
        match self {
            Self::SvelteKit => &SVELTEKIT_SPEC,
            Self::Dioxus => &DIOXUS_SPEC,
        }
    }

    pub fn default_output_dir(self) -> &'static str {
        self.spec().default_output_dir
    }

    pub fn display_name(self) -> &'static str {
        self.spec().display_name
    }

    pub fn post_generate(self, frontend_dir: &Path) -> Result<()> {
        (self.spec().post_generate)(frontend_dir)
    }

    pub fn extra_format(self, project_dir: &Path) -> Result<bool> {
        (self.spec().extra_format)(project_dir)
    }

    pub fn generate_bindings(self, input: &BindingGeneratorInput<'_>) -> Result<()> {
        (self.spec().generate_bindings)(input)
    }
}

impl fmt::Display for FrontendTarget {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.spec().id)
    }
}

impl FromStr for FrontendTarget {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.trim().to_ascii_lowercase().as_str() {
            "svelte" | "sveltekit" => Ok(Self::SvelteKit),
            "dioxus" => Ok(Self::Dioxus),
            other => Err(format!(
                "unsupported frontend target '{other}' (expected 'sveltekit' or 'dioxus')"
            )),
        }
    }
}

fn detect_sveltekit(frontend_dir: &Path) -> bool {
    frontend_dir.join("svelte.config.js").exists()
        || frontend_dir.join("package.json").exists() && frontend_dir.join("src/routes").exists()
}

fn detect_dioxus(frontend_dir: &Path) -> bool {
    frontend_dir.join("Dioxus.toml").exists()
        || frontend_dir.join("dioxus.toml").exists()
        || frontend_dir.join("Cargo.toml").exists()
}

fn post_generate_sveltekit(frontend_dir: &Path) -> Result<()> {
    let _ = Command::new("bunx")
        .args(["svelte-kit", "sync"])
        .current_dir(frontend_dir)
        .output();
    Ok(())
}

fn no_post_generate(_frontend_dir: &Path) -> Result<()> {
    Ok(())
}

fn no_extra_format(_project_dir: &Path) -> Result<bool> {
    Ok(false)
}

fn format_dioxus_frontend(project_dir: &Path) -> Result<bool> {
    let output = Command::new("cargo")
        .args(["fmt", "--manifest-path", "frontend/Cargo.toml"])
        .current_dir(project_dir)
        .output()?;

    Ok(output.status.success())
}