create-lamdera-app-rs 0.1.0

A CLI tool to scaffold Lamdera applications with Tailwind CSS, authentication, i18n, and testing
Documentation
use crate::error::Result;
use colored::*;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

const TEMPLATE_DIR: &str = "templates";

pub fn get_template_path(simple: bool) -> PathBuf {
    let template_name = if simple {
        "boilerplate-simple"
    } else {
        "boilerplate"
    };

    // Try multiple locations:
    // 1. Relative to executable (works for cargo run and installed binary)
    // 2. In system share directory (for package managers)
    // 3. Current directory (fallback)

    let candidates = vec![
        // Relative to executable - go up from target/debug or target/release
        {
            if let Ok(exe_path) = std::env::current_exe() {
                if let Some(exe_dir) = exe_path.parent() {
                    // If in target/debug or target/release, go up 2 levels
                    if exe_dir.ends_with("debug") || exe_dir.ends_with("release") {
                        if let Some(target_dir) = exe_dir.parent() {
                            if let Some(project_root) = target_dir.parent() {
                                return project_root.join(TEMPLATE_DIR).join(template_name);
                            }
                        }
                    }
                    // Otherwise templates are next to the binary
                    exe_dir.join(TEMPLATE_DIR).join(template_name)
                } else {
                    PathBuf::new()
                }
            } else {
                PathBuf::new()
            }
        },
        // System install
        PathBuf::from("/usr/local/share/create-lamdera-app")
            .join(TEMPLATE_DIR)
            .join(template_name),
        // Current directory
        PathBuf::from(TEMPLATE_DIR).join(template_name),
    ];

    for candidate in &candidates {
        if candidate.exists() {
            return candidate.clone();
        }
    }

    // Fallback to first candidate
    candidates[0].clone()
}

pub fn copy_boilerplate(project_path: &Path, simple: bool) -> Result<()> {
    let template_path = get_template_path(simple);

    if !template_path.exists() {
        return Err(anyhow::anyhow!(
            "Boilerplate template not found at {:?}",
            template_path
        ));
    }

    let message = if simple {
        "Setting up simple boilerplate project (no counter/chat demos)..."
    } else {
        "Setting up boilerplate project..."
    };
    println!("{}", message.blue());

    copy_recursive(&template_path, project_path)?;

    Ok(())
}

fn copy_recursive(src: &Path, dest: &Path) -> Result<()> {
    for entry in WalkDir::new(src).into_iter().filter_entry(|e| {
        let file_name = e.file_name().to_string_lossy();
        // Skip .DS_Store, elm-stuff, node_modules, .git, .lamdera
        !matches!(
            file_name.as_ref(),
            ".DS_Store" | "elm-stuff" | "node_modules" | ".git" | ".lamdera"
        )
    }) {
        let entry = entry?;
        let path = entry.path();
        let relative_path = path.strip_prefix(src)?;
        let dest_path = dest.join(relative_path);

        if entry.file_type().is_dir() {
            fs::create_dir_all(&dest_path)?;
        } else {
            // Rename gitignore to .gitignore when copying
            let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            let final_dest_path = if file_name == "gitignore" {
                dest_path.parent().unwrap().join(".gitignore")
            } else {
                dest_path
            };

            if let Some(parent) = final_dest_path.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(path, &final_dest_path)?;
        }
    }

    Ok(())
}

pub fn update_package_json(project_path: &Path, package_manager: &str) -> Result<()> {
    let package_json_path = project_path.join("package.json");

    if !package_json_path.exists() {
        return Ok(());
    }

    let content = fs::read_to_string(&package_json_path)?;
    let mut package_json: serde_json::Value = serde_json::from_str(&content)?;

    let runner = if package_manager == "bun" { "bunx" } else { "npx" };

    // Update scripts
    if let Some(scripts) = package_json.get_mut("scripts").and_then(|s| s.as_object_mut()) {
        if let Some(start) = scripts.get_mut("start") {
            *start = serde_json::Value::String(format!(
                "concurrently \"{} tailwindcss -i ./src/styles.css -o ./public/styles.css --watch\" \"./lamdera-dev-watch.sh\"",
                runner
            ));
        }
        if let Some(start_hot) = scripts.get_mut("start:hot") {
            *start_hot = serde_json::Value::String(format!(
                "concurrently \"{} tailwindcss -i ./src/styles.css -o ./public/styles.css --watch\" \"PORT=8001 ./lamdera-dev-watch.sh\"",
                runner
            ));
        }
    }

    let updated_content = serde_json::to_string_pretty(&package_json)?;
    fs::write(&package_json_path, updated_content)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_template_path() {
        let path = get_template_path(false);
        assert!(path.to_string_lossy().contains("boilerplate"));

        let simple_path = get_template_path(true);
        assert!(simple_path.to_string_lossy().contains("boilerplate-simple"));
    }
}