fncc-core 0.2.0

Parser and codegen for the fncc native UI framework — compiles .fui files to GPUI Rust code
Documentation
pub mod codegen;
pub mod config;
pub mod parser;

pub use parser::parse;

use anyhow::{Context, Result};
use std::path::Path;

/// Main entry point: parse all .fui files under `ui_dir` and write
/// generated Rust code into `out_file`.
pub fn generate_all(ui_dir: &Path, out_file: &Path) -> Result<()> {
    let mut output = String::new();
    output.push_str("// Generated by fncc-core. Do not edit.\n\n");

    let mut file_id = 0;
    let entries = std::fs::read_dir(ui_dir).context("failed to read ui dir")?;
    for entry in entries {
        let entry = entry.context("failed to read entry")?;
        let path = entry.path();
        if path.extension().is_some_and(|ext| ext == "fui") {
            let source = std::fs::read_to_string(&path).with_context(|| format!("failed to read {:?}", path))?;
            let ast = parser::parse(&source).map_err(|e| anyhow::anyhow!("failed to parse {:?}: {e}", path))?;
            let generated = codegen::generate_with_id(&ast, file_id);
            output.push_str(&generated);
            output.push('\n');
            file_id += 1;
        }
    }

    std::fs::write(out_file, &output).context("failed to write generated file")?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicUsize, Ordering};

    static LIB_TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn test_dir() -> (PathBuf, PathBuf) {
        let id = LIB_TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
        let dir = std::env::temp_dir().join(format!("fncc_lib_test_{id}"));
        let ui_dir = dir.join("ui");
        std::fs::create_dir_all(&ui_dir).unwrap();
        (dir, ui_dir)
    }

    #[test]
    fn test_generate_all_creates_output_file() {
        let (dir, ui_dir) = test_dir();
        std::fs::write(ui_dir.join("App.fui"), "<Text>hello</Text>").unwrap();
        let out_file = dir.join("out.rs");

        generate_all(&ui_dir, &out_file).unwrap();

        assert!(out_file.exists());
        let content = std::fs::read_to_string(&out_file).unwrap();
        assert!(content.contains("// Generated by fncc-core. Do not edit."));
        assert!(content.contains("pub fn render_text()"));
        std::fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn test_generate_all_ignores_non_fui_files() {
        let (dir, ui_dir) = test_dir();
        std::fs::write(ui_dir.join("App.fui"), "<A></A>").unwrap();
        std::fs::write(ui_dir.join("notes.txt"), "not a fui").unwrap();
        std::fs::write(ui_dir.join("main.rs"), "fn main() {}").unwrap();
        let out_file = dir.join("out.rs");

        generate_all(&ui_dir, &out_file).unwrap();

        let content = std::fs::read_to_string(&out_file).unwrap();
        assert!(content.contains("render_a"));
        assert!(!content.contains("notes"));
        assert!(!content.contains("main"));
        std::fs::remove_dir_all(&dir).unwrap();
    }
}