alef 0.23.44

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::codegen::naming::to_node_name;
use crate::core::backend::GeneratedFile;
use crate::core::config::{NodeCapsuleTypeConfig, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use std::collections::HashMap;
use std::path::PathBuf;

pub(super) fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    // Collect all exported names from the native module.
    // These are used to construct named re-exports that work with both CJS and ESM.
    let mut value_names = std::collections::BTreeSet::new();
    let mut type_names = std::collections::BTreeSet::new();

    // Collect struct and class types (skip traits and capsule types)
    let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
        .node
        .as_ref()
        .map(|c| c.capsule_types.clone())
        .unwrap_or_default();
    for typ in api.types.iter() {
        // The napi binding filters out Builder/Update DTOs (see the
        // filters at lines 382/643/735/776) — exclude them here too so
        // the typescript wrapper does not re-export names that the
        // native module never emits.
        if !typ.is_trait
            && !capsule_types.contains_key(&typ.name)
            && !typ.name.ends_with("Builder")
            && !typ.name.ends_with("Update")
        {
            if typ.is_opaque {
                value_names.insert(typ.name.clone());
            } else {
                type_names.insert(typ.name.clone());
            }
        }
    }

    // Collect enums
    for enum_def in &api.enums {
        if enum_def.variants.iter().any(|v| !v.fields.is_empty()) {
            type_names.insert(enum_def.name.clone());
        } else {
            value_names.insert(enum_def.name.clone());
        }
    }

    // Runtime classes/enums can also be used as types. Keep type-only
    // exports disjoint from value exports so the wrapper barrel never emits
    // the same name in both export groups.
    for name in &value_names {
        type_names.remove(name);
    }

    // Collect functions (with snake_case → camelCase conversion for JS naming)
    let exclude_functions: ahash::AHashSet<String> = config
        .node
        .as_ref()
        .map(|c| c.exclude_functions.iter().cloned().collect())
        .unwrap_or_default();
    for func in &api.functions {
        if !exclude_functions.contains(&func.name) {
            value_names.insert(to_node_name(&func.name));
        }
    }

    // Include trait-bridge register/unregister/clear functions
    for bridge in &config.trait_bridges {
        if let Some(name) = bridge.register_fn.as_deref() {
            value_names.insert(to_node_name(name));
        }
        if let Some(name) = bridge.unregister_fn.as_deref() {
            value_names.insert(to_node_name(name));
        }
        if let Some(name) = bridge.clear_fn.as_deref() {
            value_names.insert(to_node_name(name));
        }
    }

    // Generate TypeScript re-export file using explicit named re-exports.
    // This works with both CJS and ESM because we use `export { name } from "module"`
    // which TypeScript understands regardless of the underlying CJS/ESM implementation.
    let package_name = config.node_package_name();
    let mut lines = vec![];

    // Export runtime values (functions, classes, enums) as regular exports.
    if !value_names.is_empty() {
        lines.push("export {".to_string());
        for name in &value_names {
            lines.push(format!("  {name},"));
        }
        lines.push(format!("}} from \"{}\";", package_name));
        lines.push("".to_string());
    }

    // Export types as type-only exports (TypeScript 4.5+)
    if !type_names.is_empty() {
        lines.push("export type {".to_string());
        for name in &type_names {
            lines.push(format!("  {name},"));
        }
        lines.push(format!("}} from \"{}\";", package_name));
        lines.push("".to_string());
    }

    // Note: custom_modules (if any) are not included in this wrapper because:
    // 1. packages/typescript/src/ is the wrapper package, which only re-exports from the native binding
    // 2. custom modules (e.g., helpers) are shipped in dist/ by the build system, not generated by alef
    // 3. including them here would require them to exist in src/, which they don't

    let content = lines.join("\n");

    // Output path: packages/typescript/src/index.ts
    let output_path = PathBuf::from("packages/typescript/src/index.ts");

    Ok(vec![GeneratedFile {
        path: output_path,
        content,
        generated_header: true,
    }])
}