nixpacks 0.12.3

Generate an OCI compliant image based off app source
Documentation
use serde_json::Value;
use std::{collections::HashMap, error::Error};

use anyhow::Result;
use serde::{Deserialize, Serialize};

use crate::{
    nixpacks::{app::App, environment::Environment},
    providers::node::Workspaces,
};

use super::{NodeProvider, PackageJson};

#[derive(Debug, Deserialize, Serialize)]
pub struct TurboJson {
    pub pipeline: HashMap<String, Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct PnpmWorkspaces {
    packages: Vec<String>,
}

pub fn pnpm_workspaces(app: &App) -> Result<Vec<String>> {
    let workspaces: PnpmWorkspaces = app.read_yaml("pnpm-workspace.yaml")?;

    Ok(workspaces.packages)
}

pub struct Turborepo;

impl Turborepo {
    pub fn is_turborepo(app: &App) -> bool {
        app.includes_file("turbo.json")
    }

    pub fn get_config(app: &App) -> Result<TurboJson> {
        app.read_json("turbo.json")
    }

    fn get_pipeline_cmd(cfg: &TurboJson, name: &str) -> Option<String> {
        if cfg.pipeline.contains_key(name) {
            Some(format!("npx turbo run {}", name))
        } else {
            None
        }
    }

    pub fn get_build_cmd(cfg: &TurboJson) -> Option<String> {
        Turborepo::get_pipeline_cmd(cfg, "build")
    }

    pub fn get_actual_build_cmd(
        app: &App,
        env: &Environment,
    ) -> Result<Option<String>, Box<dyn Error>> {
        let turbo_cfg = Turborepo::get_config(app)?;
        let dlx = NodeProvider::get_package_manager_dlx_command(app);
        if let Some(build_cmd) = Turborepo::get_build_cmd(&turbo_cfg) {
            return Ok(Some(build_cmd));
        } else if let Some(app_name) = Turborepo::get_app_name(env) {
            return Ok(Some(format!("{} turbo run {}:build", dlx, app_name)));
        };
        Ok(None)
    }

    pub fn get_start_cmd(cfg: &TurboJson) -> Option<String> {
        Turborepo::get_pipeline_cmd(cfg, "start")
    }

    pub fn get_actual_start_cmd(
        app: &App,
        env: &Environment,
        package_json: &PackageJson,
    ) -> Result<Option<String>, Box<dyn Error>> {
        let turbo_cfg = Turborepo::get_config(app)?;
        let app_name = Turborepo::get_app_name(env);
        let pkg_manager = NodeProvider::get_package_manager(app);

        if let Some(name) = app_name {
            if Turborepo::has_app(
                app,
                if pkg_manager == "pnpm" {
                    pnpm_workspaces(app)?
                } else if let Some(Workspaces::Array(workspaces)) = &package_json.workspaces {
                    workspaces.clone()
                } else {
                    Vec::default()
                },
                &name,
            )? {
                return Ok(Some(format!(
                    "{} --workspace {} run start",
                    pkg_manager, name
                )));
            }
            println!("Warning: Turborepo app `{}` not found", name);
        }
        if let Some(start_pipeline) = Turborepo::get_start_cmd(&turbo_cfg) {
            return Ok(Some(start_pipeline));
        }
        Ok(None)
    }

    pub fn get_app_name(env: &Environment) -> Option<String> {
        env.get_config_variable("TURBO_APP_NAME")
    }

    pub fn has_app(app: &App, workspaces: Vec<String>, name: &str) -> Result<bool> {
        for glob in workspaces {
            let files = app.find_directories(&glob)?;
            for file in files {
                if file.ends_with(name) {
                    return Ok(true);
                }
            }
        }
        Ok(false)
    }
}