ts-native 0.1.4

A TypeScript to native executable compiler using Cranelift
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectConfig {
    #[serde(default)]
    pub project: ProjectInfo,
    #[serde(default)]
    pub compile: CompileConfig,
    #[serde(default)]
    pub extensions: ExtensionsConfig,
    #[serde(default)]
    pub link: LinkConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectInfo {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CompileConfig {
    #[serde(default = "default_input")]
    pub input: String,
    #[serde(default = "default_output")]
    pub output: String,
    #[serde(default)]
    pub target: Option<String>,
}

fn default_input() -> String {
    "src/main.ts".to_string()
}

fn default_output() -> String {
    "a.exe".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExtensionsConfig {
    #[serde(default = "default_true")]
    pub auto_discover: bool,
    #[serde(default)]
    pub items: Vec<ExtensionItem>,
    #[serde(default)]
    pub exclude: Vec<String>,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionItem {
    pub path: Option<String>,
    pub crate_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LinkConfig {
    #[serde(default = "default_linker")]
    pub linker: String,
    #[serde(default)]
    pub flags: Vec<String>,
}

fn default_linker() -> String {
    "auto".to_string()
}

impl ProjectConfig {
    pub fn load(path: &std::path::Path) -> Result<Self> {
        if !path.exists() {
            return Ok(Self::default());
        }
        
        let content = std::fs::read_to_string(path)?;
        let config: Self = toml::from_str(&content)?;
        Ok(config)
    }
    
    pub fn load_from_dir(dir: &std::path::Path) -> Result<Self> {
        let config_path = dir.join("ts-native.toml");
        Self::load(&config_path)
    }
    
    pub fn has_explicit_extensions(&self) -> bool {
        !self.extensions.items.is_empty()
    }
    
    pub fn should_auto_discover(&self) -> bool {
        self.extensions.auto_discover && !self.has_explicit_extensions()
    }
    
    pub fn get_excluded_extensions(&self) -> &[String] {
        &self.extensions.exclude
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_default_config() {
        let config = ProjectConfig::default();
        assert_eq!(config.compile.input, "src/main.ts");
        assert!(config.extensions.auto_discover);
        assert!(!config.has_explicit_extensions());
    }
    
    #[test]
    fn test_parse_config() {
        let config_content = r#"
[project]
name = "my-app"
version = "0.1.0"

[compile]
input = "src/app.ts"
output = "dist/app.exe"

[extensions]
auto_discover = true
exclude = ["ts-native-fs"]
"#;
        let config: ProjectConfig = toml::from_str(config_content).unwrap();
        assert_eq!(config.project.name, "my-app");
        assert_eq!(config.compile.input, "src/app.ts");
        assert!(config.should_auto_discover());
        assert!(config.get_excluded_extensions().contains(&"ts-native-fs".to_string()));
    }
}