forklaunch 1.19.1

Launch faster with forklaunch
use std::{fs::read_to_string, path::Path};

use anyhow::{Context, Result, bail};
use application_package_json::ApplicationPackageJson;
use ramhorns::{Content, Template};
use serde::Serialize;
use serde_json::{from_str, to_string_pretty};

use super::{
    manifest::{InitializableManifestConfig, ManifestData},
    pnpm_workspace::PnpmWorkspace,
    rendered_template::{RenderedTemplate, RenderedTemplatesCache},
};
use crate::{
    constants::{
        ERROR_FAILED_TO_ADD_PROJECT_METADATA_TO_PACKAGE_JSON, ERROR_FAILED_TO_PARSE_PACKAGE_JSON,
        ERROR_FAILED_TO_READ_PACKAGE_JSON, ERROR_FAILED_TO_UPDATE_APPLICATION_PACKAGE_JSON,
        ERROR_UNSUPPORTED_DATABASE, Runtime,
    },
    core::manifest::{ManifestConfig, ProjectManifestConfig},
};

pub(crate) mod application_package_json;
pub(crate) mod package_json_constants;
pub(crate) mod project_package_json;

pub(crate) fn add_project_definition_to_package_json<
    T: Content + ManifestConfig + ProjectManifestConfig + InitializableManifestConfig + Serialize,
>(
    base_path: &Path,
    manifest_data: &T,
) -> Result<String> {
    let mut full_package_json: ApplicationPackageJson = from_str(
        &read_to_string(base_path.join("package.json"))
            .with_context(|| ERROR_FAILED_TO_READ_PACKAGE_JSON)?,
    )
    .with_context(|| ERROR_FAILED_TO_PARSE_PACKAGE_JSON)?;

    if let Some(workspaces) = full_package_json.workspaces.as_mut() {
        if !workspaces.contains(&manifest_data.name()) {
            workspaces.push(manifest_data.name().clone());
        }
    }

    Ok(Template::new(
        to_string_pretty(&full_package_json)
            .with_context(|| ERROR_FAILED_TO_ADD_PROJECT_METADATA_TO_PACKAGE_JSON)?
            .as_str(),
    )?
    .render(&manifest_data))
}

pub(crate) fn update_application_package_json(
    manifest_data: &ManifestData,
    base_path: &Path,
    existing_package_json: Option<String>,
) -> Result<Option<RenderedTemplate>> {
    let mut full_package_json: ApplicationPackageJson = from_str(
        &existing_package_json.unwrap_or(
            read_to_string(base_path.join("package.json"))
                .with_context(|| ERROR_FAILED_TO_READ_PACKAGE_JSON)?,
        ),
    )
    .with_context(|| ERROR_FAILED_TO_PARSE_PACKAGE_JSON)?;

    if let ManifestData::Worker(worker_data) = manifest_data {
        if !worker_data.is_database_enabled {
            return Ok(Some(RenderedTemplate {
                path: base_path.join("package.json"),
                content: to_string_pretty(&full_package_json)?,
                context: Some(ERROR_FAILED_TO_UPDATE_APPLICATION_PACKAGE_JSON.to_string()),
            }));
        }
    }

    let scripts = full_package_json.scripts.as_mut().unwrap();

    let database = match manifest_data {
        ManifestData::Service(service_data) => service_data.database.to_string(),
        ManifestData::Worker(worker_data) => worker_data.database.clone().unwrap(),
        _ => bail!(ERROR_UNSUPPORTED_DATABASE),
    };

    let is_mongo = match manifest_data {
        ManifestData::Service(service_data) => service_data.is_mongo,
        ManifestData::Worker(worker_data) => worker_data.is_mongo,
        _ => false,
    };

    let update_docker_cmd = |cmd: &str| {
        let (docker_cmd, rest_cmd) = match cmd.split_once("&&") {
            Some((docker, rest)) => (docker.trim(), rest.trim()),
            None => (cmd.trim(), ""),
        };

        let mut services: Vec<&str> = docker_cmd
            .split("-d")
            .skip(1)
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .collect();

        if !services.contains(&database.as_str()) {
            services.push(&database);
            if is_mongo {
                services.push("mongo-init");
            }
        }

        let new_docker_cmd = format!(
            "docker compose up {}",
            services
                .iter()
                .map(|svc| format!("-d {}", svc))
                .collect::<Vec<_>>()
                .join(" ")
        );

        if rest_cmd.is_empty() {
            new_docker_cmd
        } else {
            format!("{} && {}", new_docker_cmd, rest_cmd)
        }
    };

    if let Some(init_script) = &scripts.migrate_init {
        scripts.migrate_init = Some(update_docker_cmd(&init_script));
    }

    if let Some(create_script) = &scripts.migrate_create {
        scripts.migrate_create = Some(update_docker_cmd(&create_script));
    }

    if let Some(up_script) = &scripts.migrate_up {
        scripts.migrate_up = Some(update_docker_cmd(&up_script));
    }

    if let Some(down_script) = &scripts.migrate_down {
        scripts.migrate_down = Some(update_docker_cmd(&down_script));
    }

    if let Some(seed_script) = &scripts.seed {
        scripts.seed = Some(update_docker_cmd(&seed_script));
    }

    Ok(Some(RenderedTemplate {
        path: base_path.join("package.json"),
        content: to_string_pretty(&full_package_json)?,
        context: Some(ERROR_FAILED_TO_UPDATE_APPLICATION_PACKAGE_JSON.to_string()),
    }))
}

pub(crate) fn replace_project_in_workspace_definition(
    application_base_path: &Path,
    application_package_json: &mut ApplicationPackageJson,
    runtime: &Runtime,
    existing_project_name: &str,
    new_project_name: &str,
    rendered_templates_cache: &mut RenderedTemplatesCache,
) -> Result<()> {
    match runtime {
        Runtime::Bun => {
            if let Some(workspaces) = application_package_json.workspaces.as_mut() {
                workspaces.remove(
                    workspaces
                        .iter()
                        .position(|name| name == existing_project_name)
                        .unwrap(),
                );
                workspaces.push(new_project_name.to_string());
            }
        }
        Runtime::Node => {
            let workspace_definition = rendered_templates_cache
                .get(Path::new(application_base_path).join("pnpm-workspace.yaml"))?
                .unwrap()
                .content;

            let mut workspace_definition: PnpmWorkspace =
                serde_yml::from_str(&workspace_definition)
                    .with_context(|| ERROR_FAILED_TO_PARSE_PACKAGE_JSON)?;

            workspace_definition.packages.remove(
                workspace_definition
                    .packages
                    .iter()
                    .position(|name| name == existing_project_name)
                    .unwrap(),
            );

            workspace_definition
                .packages
                .push(new_project_name.to_string());

            rendered_templates_cache.insert(
                application_base_path
                    .join("pnpm-workspace.yaml")
                    .to_string_lossy(),
                RenderedTemplate {
                    path: application_base_path.join("pnpm-workspace.yaml"),
                    content: serde_yml::to_string(&workspace_definition)?,
                    context: None,
                },
            );
        }
    }

    Ok(())
}

pub(crate) fn remove_project_definition_to_package_json(
    base_path: &Path,
    project_name: &str,
) -> Result<String> {
    let mut full_package_json: ApplicationPackageJson = from_str(
        &read_to_string(base_path.join("package.json"))
            .with_context(|| ERROR_FAILED_TO_READ_PACKAGE_JSON)?,
    )
    .with_context(|| ERROR_FAILED_TO_PARSE_PACKAGE_JSON)?;

    if let Some(workspaces) = full_package_json.workspaces.as_mut() {
        if let Some(position) = workspaces.iter().position(|name| name == project_name) {
            workspaces.remove(position);
        }
    }

    Ok(to_string_pretty(&full_package_json)
        .with_context(|| ERROR_FAILED_TO_ADD_PROJECT_METADATA_TO_PACKAGE_JSON)?)
}