alef 0.24.17

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, TraitBridgeConfig};
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");

    let mut files = vec![GeneratedFile {
        path: output_path,
        content,
        generated_header: true,
    }];

    // Generate TypeScript trait bridge files (interfaces and adapters)
    let bridges: Vec<(String, &TraitBridgeConfig, &crate::core::ir::TypeDef)> = config
        .trait_bridges
        .iter()
        .filter_map(|bridge_cfg| {
            api.types
                .iter()
                .find(|t| t.is_trait && t.name == bridge_cfg.trait_name)
                .map(|trait_def| (bridge_cfg.trait_name.clone(), bridge_cfg, trait_def))
        })
        .collect();

    for (trait_name, _bridge_cfg, trait_def) in &bridges {
        let ts_file_content = gen_single_typescript_trait_bridge(trait_name, trait_def);
        let ts_file_path = PathBuf::from(format!("packages/typescript/src/bridges/{}Bridge.ts", trait_name));
        files.push(GeneratedFile {
            path: ts_file_path,
            content: ts_file_content,
            generated_header: true,
        });
    }

    Ok(files)
}

/// Generate a single TypeScript trait bridge file with interface and adapter class.
fn gen_single_typescript_trait_bridge(trait_name: &str, trait_def: &crate::core::ir::TypeDef) -> String {
    use heck::ToLowerCamelCase;

    let bridge_interface_name = format!("{}Bridge", trait_name);
    let adapter_class_name = format!("{}Adapter", trait_name);

    let mut interface_methods = String::new();
    let mut adapter_methods = String::new();

    for method in &trait_def.methods {
        let method_camel = method.name.to_lower_camel_case();
        let has_error = method.error_type.is_some();

        // Generate parameter list
        let params = method
            .params
            .iter()
            .map(|p| {
                let param_name = p.name.to_lower_camel_case();
                format!("{}: string", param_name) // All params marshalled as JSON strings at boundary
            })
            .collect::<Vec<_>>()
            .join(", ");

        // Generate return type (all returns are strings for JSON marshalling)
        let return_type = "string";
        let optional_mark = if has_error { "?" } else { "" };

        // Interface method - make it optional if it can throw (error_type.is_some())
        interface_methods.push_str(&format!(
            "  {}{}({}): {};\n",
            method_camel, optional_mark, params, return_type
        ));

        // Adapter method delegate
        let call_args = method
            .params
            .iter()
            .map(|p| p.name.to_lower_camel_case())
            .collect::<Vec<_>>()
            .join(", ");

        adapter_methods.push_str(&format!(
            "  async {}({}): Promise<{}> {{\n",
            method_camel, params, return_type
        ));
        adapter_methods.push_str(&format!(
            "    try {{\n      const result = await this.bridge.{}({});\n",
            method_camel, call_args
        ));
        adapter_methods.push_str("      return JSON.stringify({ ok: result });\n");
        adapter_methods.push_str("    } catch (e) {\n");
        adapter_methods.push_str("      return JSON.stringify({ err: String(e) });\n");
        adapter_methods.push_str("    }\n");
        adapter_methods.push_str("  }\n\n");
    }

    // Generate full file content
    let mut content = String::new();
    content.push_str("/**\n");
    content.push_str(" * Generated by alef. Do not edit by hand.\n");
    content.push_str(" * \n");
    content.push_str(" * TypeScript trait bridge for outbound plugin implementations.\n");
    content.push_str(" */\n\n");

    // Interface
    content.push_str(&format!(
        "export interface {} {{\n  name(): string;\n  version(): string;\n  initialize(): Promise<void>;\n  shutdown(): Promise<void>;\n{}}}",
        bridge_interface_name, interface_methods
    ));

    content.push_str("\n\n");

    // Adapter class
    content.push_str(&format!(
        "export class {} {{\n  constructor(private bridge: {}) {{}}\n\n{}}}",
        adapter_class_name, bridge_interface_name, adapter_methods
    ));

    content
}