flowjs-rs 0.0.0-alpha.0

Generate Flow type declarations from Rust types
Documentation
//! File export logic for Flow type declarations.

use crate::{Config, Flow, TypeVisitor};
use std::any::TypeId;
use std::collections::HashSet;
use std::path::Path;

/// Export error.
#[derive(Debug, thiserror::Error)]
pub enum ExportError {
    #[error("type `{0}` cannot be exported")]
    CannotBeExported(&'static str),
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Fmt(#[from] std::fmt::Error),
}

/// Write a Flow type declaration to a file.
pub fn export_to<T: Flow + 'static + ?Sized>(cfg: &Config, path: &Path) -> Result<(), ExportError> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let type_name = T::ident(cfg);

    if path.exists() {
        let existing = std::fs::read_to_string(path)?;
        let marker = format!("type {type_name}");

        if existing.contains(&marker) {
            return Ok(());
        }

        // Same file — append only the declaration, skip header and imports
        let decl_line = format_decl::<T>(cfg);
        let mut merged = existing;
        if !merged.ends_with('\n') {
            merged.push('\n');
        }
        merged.push('\n');
        if let Some(docs) = T::docs() {
            merged.push_str(&format_docs(&docs));
        }
        merged.push_str(&decl_line);
        merged.push('\n');
        std::fs::write(path, merged)?;
    } else {
        let content = export_to_string::<T>(cfg)?;
        std::fs::write(path, content)?;
    }

    Ok(())
}

/// Export `T` together with all of its dependencies.
pub fn export_all_into<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<(), ExportError> {
    let mut seen = HashSet::new();
    export_recursive::<T>(cfg, &mut seen)
}

struct Visit<'a> {
    cfg: &'a Config,
    seen: &'a mut HashSet<TypeId>,
    error: Option<ExportError>,
}

impl TypeVisitor for Visit<'_> {
    fn visit<T: Flow + 'static + ?Sized>(&mut self) {
        if self.error.is_some() || <T as crate::Flow>::output_path().is_none() {
            return;
        }
        self.error = export_recursive::<T>(self.cfg, self.seen).err();
    }
}

fn export_recursive<T: Flow + ?Sized + 'static>(
    cfg: &Config,
    seen: &mut HashSet<TypeId>,
) -> Result<(), ExportError> {
    if !seen.insert(TypeId::of::<T>()) {
        return Ok(());
    }

    // Export dependencies first so they exist before the parent
    let mut visitor = Visit {
        cfg,
        seen,
        error: None,
    };
    <T as crate::Flow>::visit_dependencies(&mut visitor);

    if let Some(e) = visitor.error {
        return Err(e);
    }

    let relative = <T as crate::Flow>::output_path()
        .ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
    let path = cfg.out_dir().join(relative);
    export_to::<T>(cfg, &path)
}

/// Render the full file content for type `T` as a string.
///
/// Includes the `// @flow` header, import statements for dependencies that
/// export to different files, and the type declaration.
pub fn export_to_string<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<String, ExportError> {
    let mut content = String::from("// @flow\n// Generated by flowjs-rs. Do not edit.\n\n");

    let self_id = TypeId::of::<T>();
    let self_path = <T as crate::Flow>::output_path();
    let deps = T::dependencies(cfg);
    let mut seen_imports = HashSet::new();

    for dep in &deps {
        if dep.type_id == self_id || !seen_imports.insert(dep.type_id) {
            continue;
        }

        // Skip import if dependency exports to the same file
        if let Some(ref self_p) = self_path {
            if &dep.output_path == self_p {
                continue;
            }
        }

        let import_path = dep.output_path.to_str().unwrap_or(&dep.flow_name);
        content.push_str(&format!(
            "import type {{ {} }} from './{import_path}';\n",
            dep.flow_name
        ));
    }
    if !seen_imports.is_empty() {
        content.push('\n');
    }

    if let Some(docs) = T::docs() {
        content.push_str(&format_docs(&docs));
    }

    content.push_str(&format_decl::<T>(cfg));
    content.push('\n');

    Ok(content)
}

/// Format the declaration line with `export` prefix.
fn format_decl<T: Flow + ?Sized>(cfg: &Config) -> String {
    let decl = T::decl(cfg);
    if decl.starts_with("declare export") {
        decl
    } else {
        format!("export {decl}")
    }
}

/// Format doc comments as JSDoc.
fn format_docs(docs: &str) -> String {
    let mut out = String::from("/**\n");
    for line in docs.lines() {
        out.push_str(&format!(" * {line}\n"));
    }
    out.push_str(" */\n");
    out
}