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 {
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))?;
if let Ok(project_type) = Self::detect_nextjs(&project_path).await {
return Ok(project_type);
}
if let Ok(project_type) = Self::detect_nodejs(&project_path).await {
return Ok(project_type);
}
if let Ok(project_type) = Self::detect_rust(&project_path).await {
return Ok(project_type);
}
Ok(ProjectType::Unknown)
}
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)?;
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,
})
}
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,
})
}
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,
})
}
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,
})
}
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,
})
}
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,
}