mockforge-ui 0.3.2

Admin UI for MockForge - web-based interface for managing mock servers
Documentation
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;

#[derive(Deserialize, Debug)]
struct ManifestEntry {
    file: String,
    css: Option<Vec<String>>,
}

fn main() {
    // Generate version information using vergen
    if let Err(e) = vergen::EmitBuilder::builder().build_timestamp().git_sha(true).emit() {
        println!("cargo:warning=Failed to generate version info: {}", e);
    }
    println!("cargo:rerun-if-changed=ui/build.rs");
    println!("cargo:rerun-if-changed=ui/src/");

    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let ui_dist_path = Path::new(&crate_dir).join("ui/dist");
    let ui_public_path = Path::new(&crate_dir).join("ui/public");

    // Ensure dist directory exists
    if !ui_dist_path.exists() {
        fs::create_dir_all(&ui_dist_path).expect("Failed to create ui/dist directory");
    }

    // Copy PWA manifest.json and sw.js from public to dist if they don't exist in dist
    // Note: Vite will generate its own manifest.json during build, so we need to preserve
    // the PWA manifest with a different name or handle it separately
    let pwa_manifest_source = ui_public_path.join("manifest.json");
    let pwa_manifest_dest = ui_dist_path.join("pwa-manifest.json");
    let sw_source = ui_public_path.join("sw.js");
    let sw_dest = ui_dist_path.join("sw.js");

    if pwa_manifest_source.exists() && !pwa_manifest_dest.exists() {
        fs::copy(&pwa_manifest_source, &pwa_manifest_dest)
            .expect("Failed to copy PWA manifest.json from public to dist");
    }

    if sw_source.exists() && !sw_dest.exists() {
        fs::copy(&sw_source, &sw_dest).expect("Failed to copy sw.js from public to dist");
    }

    // Try to run the UI build script, but don't fail if it doesn't exist or fails
    // This allows the crate to compile even when the UI hasn't been built
    let ui_build_script = Path::new(&crate_dir).join("build_ui.sh");
    if ui_build_script.exists() {
        let status = Command::new("bash").arg(&ui_build_script).status();

        if let Ok(status) = status {
            if !status.success() {
                println!(
                    "cargo:warning=UI build script failed, but continuing with fallback files"
                );
            }
        } else {
            println!(
                "cargo:warning=Failed to run UI build script, but continuing with fallback files"
            );
        }
    } else {
        println!("cargo:warning=UI build script not found, using fallback files from public/");
    }

    println!("cargo:rerun-if-changed={}", ui_dist_path.join("manifest.json").display());
    println!("cargo:rerun-if-changed={}", ui_dist_path.join("pwa-manifest.json").display());
    println!("cargo:rerun-if-changed={}", ui_dist_path.join("sw.js").display());
    println!("cargo:rerun-if-changed={}", ui_dist_path.join("assets").display());

    let out_dir = env::var_os("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("asset_paths.rs");

    // Check for Vite build manifest (generated by Vite during build)
    let vite_manifest_path = ui_dist_path.join("manifest.json");
    let assets_dir = ui_dist_path.join("assets");

    // Try to parse the Vite build manifest if it exists
    // The Vite manifest has a different structure than the PWA manifest
    if vite_manifest_path.exists() {
        // Try to parse as Vite build manifest
        if let Ok(manifest_content) = fs::read_to_string(&vite_manifest_path) {
            if let Ok(manifest) =
                serde_json::from_str::<HashMap<String, ManifestEntry>>(&manifest_content)
            {
                // This is a Vite build manifest
                let entry = manifest
                    .get("index.html")
                    .expect("Could not find index.html entry in Vite manifest.json");

                let js_path = ui_dist_path.join(&entry.file);
                let css_path = entry
                    .css
                    .as_ref()
                    .and_then(|files| files.first())
                    .map(|file| ui_dist_path.join(file));

                let css_content = if let Some(path) = css_path {
                    format!(
                        "pub fn get_admin_css() -> &'static str {{    include_str!(r\"{}\")\n}}",
                        path.display()
                    )
                } else {
                    "pub fn get_admin_css() -> &'static str { \"\" }\n".to_string()
                };

                let js_content = format!(
                    "pub fn get_admin_js() -> &'static str {{    include_str!(r\"{}\")\n}}",
                    js_path.display()
                );

                fs::write(&dest_path, format!("{}\n\n{}", css_content, js_content)).unwrap();
                return; // Successfully processed Vite manifest
            }
            // If parsing failed, it might be the PWA manifest, fall through to assets scan
        }
    }

    // Fallback: scan assets directory if it exists
    if assets_dir.exists() {
        // Fallback: scan assets directory and generate lookup map for all JS/CSS files
        // Note: HashMap is already imported in assets.rs, so don't import it here
        let mut asset_map = String::from(
            "pub fn get_asset_map() -> std::collections::HashMap<&'static str, &'static str> {\n",
        );
        asset_map.push_str("    let mut map = std::collections::HashMap::new();\n");

        // Read all files in assets directory
        if let Ok(entries) = fs::read_dir(&assets_dir) {
            for entry in entries.flatten() {
                if let Some(filename) = entry.path().file_name().and_then(|n| n.to_str()) {
                    if filename.ends_with(".js") || filename.ends_with(".css") {
                        let asset_path = entry.path();
                        asset_map.push_str(&format!(
                            "    map.insert(\"{}\", include_str!(r\"{}\"));\n",
                            filename,
                            asset_path.display()
                        ));
                    }
                }
            }
        }

        asset_map.push_str("    map\n");
        asset_map.push_str("}\n");

        // Also generate the CSS and JS functions
        let css_content = format!(
            "pub fn get_admin_css() -> &'static str {{    include_str!(r\"{}\")\n}}",
            ui_dist_path.join("assets/index.css").display()
        );
        let js_content = format!(
            "pub fn get_admin_js() -> &'static str {{    include_str!(r\"{}\")\n}}",
            ui_dist_path.join("assets/index.js").display()
        );

        fs::write(&dest_path, format!("{}\n\n{}\n\n{}", css_content, js_content, asset_map))
            .unwrap();
    } else {
        // UI not built, create dummy functions
        let content = "
            pub fn get_admin_css() -> &'static str { \"\" }
            pub fn get_admin_js() -> &'static str { \"\" }
            pub fn get_asset_map() -> std::collections::HashMap<&'static str, &'static str> {
                std::collections::HashMap::new()
            }
        ";
        fs::write(&dest_path, content).unwrap();
    }
}