xbp 0.5.4

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! Project type detection module
//!
//! This module analyzes project directories to determine the type of application
//! and extract relevant configuration information.

use serde::{Deserialize, Serialize};
use std::path::Path;
use std::fs;
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProjectType {
    NextJs {
        package_json: PackageJsonInfo,
        has_next_config: bool,
    },
    NodeJs {
        package_json: PackageJsonInfo,
    },
    Rust {
        cargo_toml: CargoTomlInfo,
    },
    Unknown,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageJsonInfo {
    pub name: String,
    pub version: String,
    pub scripts: std::collections::HashMap<String, String>,
    pub dependencies: std::collections::HashMap<String, String>,
    pub dev_dependencies: std::collections::HashMap<String, String>,
    pub main: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CargoTomlInfo {
    pub name: String,
    pub version: String,
    pub description: Option<String>,
    pub authors: Vec<String>,
    pub edition: Option<String>,
}

pub struct ProjectDetector;

impl ProjectDetector {
    /// Detect the project type by analyzing the directory structure and configuration files
    pub async fn detect_project_type(project_path: &Path) -> Result<ProjectType, String> {
        let project_path = project_path.canonicalize()
            .map_err(|e| format!("Failed to resolve project path: {}", e))?;

        // Check for Next.js project
        if let Ok(project_type) = Self::detect_nextjs(&project_path).await {
            return Ok(project_type);
        }

        // Check for Node.js project
        if let Ok(project_type) = Self::detect_nodejs(&project_path).await {
            return Ok(project_type);
        }

        // Check for Rust project
        if let Ok(project_type) = Self::detect_rust(&project_path).await {
            return Ok(project_type);
        }

        Ok(ProjectType::Unknown)
    }

    /// Detect Next.js project by looking for .next folder and next.js dependencies
    async fn detect_nextjs(project_path: &Path) -> Result<ProjectType, String> {
        let package_json_path = project_path.join("package.json");
        let next_config_path = project_path.join("next.config.js");
        let next_config_mjs_path = project_path.join("next.config.mjs");
        let next_dir = project_path.join(".next");

        if !package_json_path.exists() {
            return Err("No package.json found".to_string());
        }

        let package_json_info = Self::parse_package_json(&package_json_path)?;

        // Check if Next.js is in dependencies
        let has_next = package_json_info.dependencies.contains_key("next") ||
                      package_json_info.dev_dependencies.contains_key("next");

        if !has_next {
            return Err("Next.js not found in dependencies".to_string());
        }

        let has_next_config = next_config_path.exists() || 
                             next_config_mjs_path.exists() ||
                             next_dir.exists();

        Ok(ProjectType::NextJs {
            package_json: package_json_info,
            has_next_config,
        })
    }

    /// Detect Node.js project by looking for package.json
    async fn detect_nodejs(project_path: &Path) -> Result<ProjectType, String> {
        let package_json_path = project_path.join("package.json");

        if !package_json_path.exists() {
            return Err("No package.json found".to_string());
        }

        let package_json_info = Self::parse_package_json(&package_json_path)?;

        Ok(ProjectType::NodeJs {
            package_json: package_json_info,
        })
    }

    /// Detect Rust project by looking for Cargo.toml
    async fn detect_rust(project_path: &Path) -> Result<ProjectType, String> {
        let cargo_toml_path = project_path.join("Cargo.toml");

        if !cargo_toml_path.exists() {
            return Err("No Cargo.toml found".to_string());
        }

        let cargo_toml_info = Self::parse_cargo_toml(&cargo_toml_path)?;

        Ok(ProjectType::Rust {
            cargo_toml: cargo_toml_info,
        })
    }

    /// Parse package.json file and extract relevant information
    fn parse_package_json(path: &Path) -> Result<PackageJsonInfo, String> {
        let content = fs::read_to_string(path)
            .map_err(|e| format!("Failed to read package.json: {}", e))?;

        let json: Value = serde_json::from_str(&content)
            .map_err(|e| format!("Failed to parse package.json: {}", e))?;

        let name = json["name"].as_str()
            .unwrap_or("unknown")
            .to_string();

        let version = json["version"].as_str()
            .unwrap_or("0.0.0")
            .to_string();

        let scripts = json["scripts"].as_object()
            .map(|obj| {
                obj.iter()
                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                    .collect()
            })
            .unwrap_or_default();

        let dependencies = json["dependencies"].as_object()
            .map(|obj| {
                obj.iter()
                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                    .collect()
            })
            .unwrap_or_default();

        let dev_dependencies = json["devDependencies"].as_object()
            .map(|obj| {
                obj.iter()
                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                    .collect()
            })
            .unwrap_or_default();

        let main = json["main"].as_str().map(|s| s.to_string());

        Ok(PackageJsonInfo {
            name,
            version,
            scripts,
            dependencies,
            dev_dependencies,
            main,
        })
    }

    /// Parse Cargo.toml file and extract relevant information
    fn parse_cargo_toml(path: &Path) -> Result<CargoTomlInfo, String> {
        let content = fs::read_to_string(path)
            .map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;

        let toml_value: toml::Value = toml::from_str(&content)
            .map_err(|e| format!("Failed to parse Cargo.toml: {}", e))?;

        let package = toml_value.get("package")
            .ok_or("No [package] section found in Cargo.toml")?;

        let name = package.get("name")
            .and_then(|v| v.as_str())
            .ok_or("No name found in [package] section")?
            .to_string();

        let version = package.get("version")
            .and_then(|v| v.as_str())
            .unwrap_or("0.0.0")
            .to_string();

        let description = package.get("description")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let authors = package.get("authors")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str())
                    .map(|s| s.to_string())
                    .collect()
            })
            .unwrap_or_default();

        let edition = package.get("edition")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        Ok(CargoTomlInfo {
            name,
            version,
            description,
            authors,
            edition,
        })
    }

    /// Get recommended deployment configuration based on project type
    pub fn get_deployment_recommendations(project_type: &ProjectType) -> DeploymentRecommendations {
        match project_type {
            ProjectType::NextJs { package_json, .. } => {
                DeploymentRecommendations {
                    build_command: Some("pnpm run build".to_string()),
                    start_command: Some("pnpm run start".to_string()),
                    install_command: Some("pnpm install".to_string()),
                    default_port: 3000,
                    process_name: Some(package_json.name.clone()),
                    requires_build: true,
                }
            }
            ProjectType::NodeJs { package_json } => {
                let start_cmd = package_json.scripts.get("start")
                    .map(|s| format!("pnpm run {}", s.split_whitespace().next().unwrap_or("start")))
                    .or_else(|| package_json.main.as_ref().map(|m| format!("node {}", m)))
                    .unwrap_or_else(|| "pnpm run start".to_string());

                DeploymentRecommendations {
                    build_command: package_json.scripts.get("build").map(|_| "pnpm run build".to_string()),
                    start_command: Some(start_cmd),
                    install_command: Some("pnpm install".to_string()),
                    default_port: 3000,
                    process_name: Some(package_json.name.clone()),
                    requires_build: package_json.scripts.contains_key("build"),
                }
            }
            ProjectType::Rust { cargo_toml } => {
                DeploymentRecommendations {
                    build_command: Some("cargo build --release".to_string()),
                    start_command: Some(format!("./target/release/{}", cargo_toml.name)),
                    install_command: None,
                    default_port: 8080,
                    process_name: Some(cargo_toml.name.clone()),
                    requires_build: true,
                }
            }
            ProjectType::Unknown => {
                DeploymentRecommendations {
                    build_command: None,
                    start_command: None,
                    install_command: None,
                    default_port: 8080,
                    process_name: None,
                    requires_build: false,
                }
            }
        }
    }
}

#[derive(Debug, Clone)]
pub struct DeploymentRecommendations {
    pub build_command: Option<String>,
    pub start_command: Option<String>,
    pub install_command: Option<String>,
    pub default_port: u16,
    pub process_name: Option<String>,
    pub requires_build: bool,
}