create-lamdera-app-rs 0.1.8

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

static TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");

fn get_template_name(simple: bool) -> &'static str {
    if simple {
        "boilerplate-simple"
    } else {
        "boilerplate"
    }
}

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

    let template_dir = TEMPLATES
        .get_dir(template_name)
        .ok_or_else(|| anyhow::anyhow!("Boilerplate template '{}' not found in embedded templates", template_name))?;

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

    copy_embedded_dir(template_dir, project_path, "")?;

    Ok(())
}

fn copy_embedded_dir(dir: &include_dir::Dir, dest: &Path, prefix: &str) -> Result<()> {
    // Create directories
    for subdir in dir.dirs() {
        let dir_name = subdir.path().file_name().unwrap().to_str().unwrap();

        // Skip unwanted directories
        if matches!(dir_name, ".DS_Store" | "elm-stuff" | "node_modules" | ".git" | ".lamdera") {
            continue;
        }

        let subdir_path = dest.join(prefix).join(dir_name);
        fs::create_dir_all(&subdir_path)?;

        let new_prefix = if prefix.is_empty() {
            dir_name.to_string()
        } else {
            format!("{}/{}", prefix, dir_name)
        };

        copy_embedded_dir(subdir, dest, &new_prefix)?;
    }

    // Copy files
    for file in dir.files() {
        let file_name = file.path().file_name().unwrap().to_str().unwrap();

        // Skip unwanted files
        if file_name == ".DS_Store" {
            continue;
        }

        // Rename gitignore to .gitignore when copying
        let final_name = if file_name == "gitignore" {
            ".gitignore"
        } else {
            file_name
        };

        let file_path = dest.join(prefix).join(final_name);

        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent)?;
        }

        fs::write(&file_path, file.contents())?;
    }

    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_name() {
        assert_eq!(get_template_name(false), "boilerplate");
        assert_eq!(get_template_name(true), "boilerplate-simple");
    }

    #[test]
    fn test_embedded_templates_exist() {
        assert!(TEMPLATES.get_dir("boilerplate").is_some());
        assert!(TEMPLATES.get_dir("boilerplate-simple").is_some());
    }
}