Skip to main content

alef_backend_napi/
gen_bindings.rs

1use crate::type_map::NapiMapper;
2use ahash::AHashSet;
3use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
4use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
5use alef_codegen::naming::to_node_name;
6use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
7use alef_codegen::type_mapper::TypeMapper;
8use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
11use std::path::PathBuf;
12
13pub struct NapiBackend;
14
15impl NapiBackend {
16    fn binding_config(core_import: &str) -> RustBindingConfig<'_> {
17        RustBindingConfig {
18            struct_attrs: &["napi"],
19            field_attrs: &[],
20            struct_derives: &["Clone"],
21            method_block_attr: Some("napi"),
22            constructor_attr: "#[napi(constructor)]",
23            static_attr: None,
24            function_attr: "#[napi]",
25            enum_attrs: &["napi(string_enum)"],
26            enum_derives: &["Clone"],
27            needs_signature: false,
28            signature_prefix: "",
29            signature_suffix: "",
30            core_import,
31            async_pattern: AsyncPattern::NapiNativeAsync,
32            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
33            has_serde: false,
34            type_name_prefix: "Js",
35        }
36    }
37}
38
39impl Backend for NapiBackend {
40    fn name(&self) -> &str {
41        "napi"
42    }
43
44    fn language(&self) -> Language {
45        Language::Node
46    }
47
48    fn capabilities(&self) -> Capabilities {
49        Capabilities {
50            supports_async: true,
51            supports_classes: true,
52            supports_enums: true,
53            supports_option: true,
54            supports_result: true,
55            ..Capabilities::default()
56        }
57    }
58
59    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
60        let mapper = NapiMapper;
61        let core_import = config.core_import();
62        let cfg = Self::binding_config(&core_import);
63
64        let mut builder = RustFileBuilder::new().with_generated_header();
65        builder.add_import("napi::*");
66        builder.add_import("napi_derive::napi");
67
68        // Import traits needed for trait method dispatch
69        for trait_path in generators::collect_trait_imports(api) {
70            builder.add_import(&trait_path);
71        }
72
73        // Only import HashMap when Map-typed fields or returns are present
74        let has_maps = api
75            .types
76            .iter()
77            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
78            || api
79                .functions
80                .iter()
81                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
82        if has_maps {
83            builder.add_import("std::collections::HashMap");
84        }
85
86        // Note: custom_modules for Node are TypeScript-only re-exports
87        // (used in generate_public_api), not Rust module declarations.
88
89        // Check if any function or method is async
90        let has_async =
91            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
92
93        if has_async {
94            builder.add_item(&gen_tokio_runtime());
95        }
96
97        // Check if we have opaque types and add Arc import if needed
98        let opaque_types: AHashSet<String> = api
99            .types
100            .iter()
101            .filter(|t| t.is_opaque)
102            .map(|t| t.name.clone())
103            .collect();
104        if !opaque_types.is_empty() {
105            builder.add_import("std::sync::Arc");
106        }
107
108        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
109        // and custom constructor. Use shared generators for enums and functions,
110        // but keep struct/method generation custom.
111        for typ in &api.types {
112            if typ.is_opaque {
113                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, "Js"));
114                builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &cfg, &opaque_types));
115            } else {
116                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
117                // napi(object) structs cannot have #[napi] impl blocks.
118                // gen_struct adds Default to derives when typ.has_default is true.
119                builder.add_item(&gen_struct(typ, &mapper));
120            }
121        }
122
123        for enum_def in &api.enums {
124            builder.add_item(&gen_enum(enum_def));
125        }
126
127        for func in &api.functions {
128            // Skip functions with opaque type params — NAPI opaque structs don't implement FromNapiValue.
129            // These functions are todo!() stubs and need manual wiring via class methods instead.
130            let has_opaque_param = func.params.iter().any(|p| {
131                if let alef_core::ir::TypeRef::Named(n) = &p.ty {
132                    opaque_types.contains(n)
133                } else {
134                    false
135                }
136            });
137            if !has_opaque_param {
138                builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types));
139            }
140        }
141
142        let binding_to_core = alef_codegen::conversions::convertible_types(api);
143        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
144        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
145            type_name_prefix: "Js",
146            cast_large_ints_to_i64: true,
147            cast_f32_to_f64: true,
148            // optionalize_defaults: For types with has_default, conversion generators
149            // make all fields Option<T> and apply defaults via FromNapiValue,
150            // enabling JS users to pass partial objects and omit fields they want defaults for.
151            optionalize_defaults: true,
152            include_cfg_metadata: true,
153            ..Default::default()
154        };
155        // From/Into conversions using shared parameterized generators
156        for typ in &api.types {
157            if alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core) {
158                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
159                    typ,
160                    &core_import,
161                    &napi_conv_config,
162                ));
163            }
164            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
165                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
166                    typ,
167                    &core_import,
168                    &opaque_types,
169                    &napi_conv_config,
170                ));
171            }
172        }
173        for e in &api.enums {
174            if alef_codegen::conversions::can_generate_enum_conversion(e) {
175                builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
176                    e,
177                    &core_import,
178                    &napi_conv_config,
179                ));
180            }
181            if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
182                builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
183                    e,
184                    &core_import,
185                    &napi_conv_config,
186                ));
187            }
188        }
189
190        // Error types (variant name constants + converter functions)
191        for error in &api.errors {
192            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
193            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
194        }
195
196        // Build adapter body map (consumed by generators via body substitution)
197        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
198
199        let content = builder.build();
200
201        let output_dir = resolve_output_dir(
202            config.output.node.as_ref(),
203            &config.crate_config.name,
204            "crates/{name}-node/src/",
205        );
206
207        Ok(vec![GeneratedFile {
208            path: PathBuf::from(&output_dir).join("lib.rs"),
209            content,
210            generated_header: false,
211        }])
212    }
213
214    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
215        // Separate exports into functions (plain export) and types (export type)
216        let mut type_exports = vec![];
217        let mut function_exports = vec![];
218
219        // Collect all types (exported with Js prefix from native module) - export type
220        for typ in &api.types {
221            type_exports.push(format!("Js{}", typ.name));
222        }
223
224        // Collect all enums as value exports (runtime objects).
225        // NAPI generates const enum in .d.ts, but we post-process it to regular enum
226        // so they can be re-exported as values with verbatimModuleSyntax.
227        for enum_def in &api.enums {
228            function_exports.push(format!("Js{}", enum_def.name));
229        }
230
231        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
232        // Skip error types in the public API re-exports.
233
234        // Collect all functions (exported from native module) - plain export
235        for func in &api.functions {
236            // Convert snake_case to camelCase for JavaScript naming
237            let js_name = to_node_name(&func.name);
238            function_exports.push(js_name);
239        }
240
241        // Sort for consistent output
242        type_exports.sort();
243        function_exports.sort();
244
245        // Generate the index.ts re-export file using a single export block
246        // with inline `type` annotations for verbatimModuleSyntax compatibility.
247        let mut lines = vec![
248            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
249            "".to_string(),
250        ];
251
252        // Single export block: value exports (functions + enums) + inline type exports (structs)
253        let mut all_exports: Vec<String> = Vec::new();
254        for name in &function_exports {
255            all_exports.push(format!("  {name},"));
256        }
257        for name in &type_exports {
258            all_exports.push(format!("  type {name},"));
259        }
260        if !all_exports.is_empty() {
261            lines.push("export {".to_string());
262            lines.extend(all_exports);
263            lines.push(format!("}} from '{}';", config.node_package_name()));
264        }
265
266        // Append re-exports for custom modules (from [custom_modules] node = [...])
267        let custom_mods = config.custom_modules.for_language(Language::Node);
268        for module_name in custom_mods {
269            lines.push(format!("export * from './{module_name}';"));
270        }
271
272        let content = lines.join("\n");
273
274        // Output path: packages/typescript/src/index.ts
275        let output_path = PathBuf::from("packages/typescript/src/index.ts");
276
277        Ok(vec![GeneratedFile {
278            path: output_path,
279            content,
280            generated_header: false,
281        }])
282    }
283
284    fn build_config(&self) -> Option<BuildConfig> {
285        Some(BuildConfig {
286            tool: "napi",
287            crate_suffix: "-node",
288            depends_on_ffi: false,
289            post_build: vec![PostBuildStep::PatchFile {
290                path: "index.d.ts",
291                find: "export declare const enum",
292                replace: "export declare enum",
293            }],
294        })
295    }
296}
297
298/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
299fn gen_struct(typ: &TypeDef, mapper: &NapiMapper) -> String {
300    let mut struct_builder = StructBuilder::new(&format!("Js{}", typ.name));
301    // Use napi(object) so the struct can be used as function/method parameters (FromNapiValue)
302    struct_builder.add_attr("napi(object)");
303    struct_builder.add_derive("Clone");
304    // Types with has_default get #[derive(Default)] instead of a manual impl.
305    if typ.has_default {
306        struct_builder.add_derive("Default");
307    }
308
309    for field in &typ.fields {
310        let mapped_type = mapper.map_type(&field.ty);
311        // For types with Default, make all fields optional so JS callers
312        // can pass partial objects (missing fields get defaults).
313        let field_type = if field.optional || typ.has_default {
314            format!("Option<{}>", mapped_type)
315        } else {
316            mapped_type
317        };
318        let js_name = to_node_name(&field.name);
319        let attrs = if js_name != field.name {
320            vec![format!("napi(js_name = \"{}\")", js_name)]
321        } else {
322            vec![]
323        };
324        struct_builder.add_field(&field.name, &field_type, attrs);
325    }
326
327    // Add synthetic fields for cfg-gated fields stripped from the IR.
328    // When has_stripped_cfg_fields is true, some fields were removed during extraction
329    // because they were behind #[cfg(...)] gates in the source. We add them back as
330    // Option types so the binding exposes the complete API when the features are enabled.
331    if typ.has_stripped_cfg_fields && typ.name == "ConversionResult" {
332        // ConversionResult has a metadata: HtmlMetadata field behind #[cfg(feature = "metadata")]
333        struct_builder.add_field("metadata", "Option<JsHtmlMetadata>", vec![]);
334    }
335
336    struct_builder.build()
337}
338
339/// Generate NAPI methods for an opaque struct (delegates to self.inner).
340fn gen_opaque_struct_methods(
341    typ: &TypeDef,
342    mapper: &NapiMapper,
343    cfg: &RustBindingConfig,
344    opaque_types: &AHashSet<String>,
345) -> String {
346    let mut impl_builder = ImplBuilder::new(&format!("Js{}", typ.name));
347    impl_builder.add_attr("napi");
348
349    let (instance, statics) = partition_methods(&typ.methods);
350
351    for method in &instance {
352        impl_builder.add_method(&gen_opaque_instance_method(method, mapper, typ, cfg, opaque_types));
353    }
354    for method in &statics {
355        impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types));
356    }
357
358    impl_builder.build()
359}
360
361/// Generate an opaque instance method that delegates to self.inner.
362fn gen_opaque_instance_method(
363    method: &MethodDef,
364    mapper: &NapiMapper,
365    typ: &TypeDef,
366    cfg: &RustBindingConfig,
367    opaque_types: &AHashSet<String>,
368) -> String {
369    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
370    let return_type = mapper.map_type(&method.return_type);
371    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
372
373    let js_name = to_node_name(&method.name);
374    let js_name_attr = if js_name != method.name {
375        format!("(js_name = \"{}\")", js_name)
376    } else {
377        String::new()
378    };
379
380    let async_kw = if method.is_async { "async " } else { "" };
381
382    let type_name = &typ.name;
383    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
384    let call_args = napi_gen_call_args(&method.params, opaque_types);
385
386    // Use the shared can_auto_delegate check for opaque instance methods.
387    let opaque_can_delegate = !method.sanitized
388        && (!is_owned_receiver || typ.is_clone)
389        && method
390            .params
391            .iter()
392            .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
393        && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
394
395    let make_core_call = |method_name: &str| -> String {
396        if is_owned_receiver {
397            format!("(*self.inner).clone().{method_name}({call_args})")
398        } else {
399            format!("self.inner.{method_name}({call_args})")
400        }
401    };
402
403    let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
404
405    let async_result_wrap = napi_wrap_return(
406        "result",
407        &method.return_type,
408        type_name,
409        opaque_types,
410        true,
411        method.returns_ref,
412    );
413
414    let body = if !opaque_can_delegate {
415        // Try serde-based param conversion for methods with non-opaque Named params
416        if cfg.has_serde
417            && !method.sanitized
418            && generators::has_named_params(&method.params, opaque_types)
419            && method.error_type.is_some()
420            && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
421        {
422            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
423            let serde_bindings =
424                generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
425            let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
426            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
427            if matches!(method.return_type, TypeRef::Unit) {
428                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
429            } else {
430                let wrap = napi_wrap_return(
431                    "result",
432                    &method.return_type,
433                    type_name,
434                    opaque_types,
435                    true,
436                    method.returns_ref,
437                );
438                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
439            }
440        } else {
441            generators::gen_unimplemented_body(
442                &method.return_type,
443                &format!("{type_name}.{}", method.name),
444                method.error_type.is_some(),
445                cfg,
446                &method.params,
447            )
448        }
449    } else if method.is_async {
450        let inner_clone_line = "let inner = self.inner.clone();\n    ";
451        let core_call_str = make_async_core_call(&method.name);
452        generators::gen_async_body(
453            &core_call_str,
454            cfg,
455            method.error_type.is_some(),
456            &async_result_wrap,
457            true,
458            inner_clone_line,
459            matches!(method.return_type, TypeRef::Unit),
460        )
461    } else {
462        let core_call = make_core_call(&method.name);
463        if method.error_type.is_some() {
464            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
465            if matches!(method.return_type, TypeRef::Unit) {
466                format!("{core_call}{err_conv}?;\n    Ok(())")
467            } else {
468                let wrap = napi_wrap_return(
469                    "result",
470                    &method.return_type,
471                    type_name,
472                    opaque_types,
473                    true,
474                    method.returns_ref,
475                );
476                format!("let result = {core_call}{err_conv}?;\n    Ok({wrap})")
477            }
478        } else {
479            napi_wrap_return(
480                &core_call,
481                &method.return_type,
482                type_name,
483                opaque_types,
484                true,
485                method.returns_ref,
486            )
487        }
488    };
489
490    let mut attrs = String::new();
491    // Per-item clippy suppression: too_many_arguments when >7 params (including &self)
492    if method.params.len() + 1 > 7 {
493        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
494    }
495    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
496    if method.error_type.is_some() {
497        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
498    }
499    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
500    if generators::is_trait_method_name(&method.name) {
501        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
502    }
503    format!(
504        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n    \
505         {body}\n}}",
506        method.name
507    )
508}
509
510/// Generate a static method binding.
511fn gen_static_method(
512    method: &MethodDef,
513    mapper: &NapiMapper,
514    typ: &TypeDef,
515    cfg: &RustBindingConfig,
516    opaque_types: &AHashSet<String>,
517) -> String {
518    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
519    let return_type = mapper.map_type(&method.return_type);
520    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
521
522    let js_name = to_node_name(&method.name);
523    let js_name_attr = if js_name != method.name {
524        format!("(js_name = \"{}\")", js_name)
525    } else {
526        String::new()
527    };
528
529    let type_name = &typ.name;
530    let core_type_path = typ.rust_path.replace('-', "_");
531    let call_args = napi_gen_call_args(&method.params, opaque_types);
532    let can_delegate_static = can_auto_delegate(method, opaque_types);
533
534    let async_kw = if method.is_async { "async " } else { "" };
535
536    let body = if !can_delegate_static {
537        generators::gen_unimplemented_body(
538            &method.return_type,
539            &format!("{type_name}::{}", method.name),
540            method.error_type.is_some(),
541            cfg,
542            &method.params,
543        )
544    } else if method.is_async {
545        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
546        let return_wrap = napi_wrap_return(
547            "result",
548            &method.return_type,
549            type_name,
550            opaque_types,
551            typ.is_opaque,
552            method.returns_ref,
553        );
554        generators::gen_async_body(
555            &core_call,
556            cfg,
557            method.error_type.is_some(),
558            &return_wrap,
559            false,
560            "",
561            matches!(method.return_type, TypeRef::Unit),
562        )
563    } else {
564        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
565        if method.error_type.is_some() {
566            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
567            let wrapped = napi_wrap_return(
568                "val",
569                &method.return_type,
570                type_name,
571                opaque_types,
572                typ.is_opaque,
573                method.returns_ref,
574            );
575            if wrapped == "val" {
576                format!("{core_call}{err_conv}")
577            } else {
578                format!("{core_call}.map(|val| {wrapped}){err_conv}")
579            }
580        } else {
581            napi_wrap_return(
582                &core_call,
583                &method.return_type,
584                type_name,
585                opaque_types,
586                typ.is_opaque,
587                method.returns_ref,
588            )
589        }
590    };
591
592    let mut attrs = String::new();
593    // Per-item clippy suppression: too_many_arguments when >7 params
594    if method.params.len() > 7 {
595        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
596    }
597    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
598    if method.error_type.is_some() {
599        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
600    }
601    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
602    if generators::is_trait_method_name(&method.name) {
603        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
604    }
605    format!(
606        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
607         {body}\n}}",
608        method.name
609    )
610}
611
612/// Generate a NAPI enum definition using string_enum with Js prefix.
613fn gen_enum(enum_def: &EnumDef) -> String {
614    let mut lines = vec![
615        "#[napi(string_enum)]".to_string(),
616        "#[derive(Clone)]".to_string(),
617        format!("pub enum Js{} {{", enum_def.name),
618    ];
619
620    for variant in &enum_def.variants {
621        lines.push(format!("    {},", variant.name));
622    }
623
624    lines.push("}".to_string());
625
626    // Default impl for config constructor unwrap_or_default()
627    if let Some(first) = enum_def.variants.first() {
628        lines.push(String::new());
629        lines.push("#[allow(clippy::derivable_impls)]".to_string());
630        lines.push(format!("impl Default for Js{} {{", enum_def.name));
631        lines.push(format!("    fn default() -> Self {{ Self::{} }}", first.name));
632        lines.push("}".to_string());
633    }
634
635    lines.join("\n")
636}
637
638/// Generate a free function binding.
639fn gen_function(
640    func: &FunctionDef,
641    mapper: &NapiMapper,
642    cfg: &RustBindingConfig,
643    opaque_types: &AHashSet<String>,
644) -> String {
645    let params = function_params(&func.params, &|ty| mapper.map_type(ty));
646    let return_type = mapper.map_type(&func.return_type);
647    let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
648
649    let js_name = to_node_name(&func.name);
650    let js_name_attr = if js_name != func.name {
651        format!("(js_name = \"{}\")", js_name)
652    } else {
653        String::new()
654    };
655
656    let core_import = cfg.core_import;
657    let core_fn_path = {
658        let path = func.rust_path.replace('-', "_");
659        if path.starts_with(core_import) {
660            path
661        } else {
662            format!("{core_import}::{}", func.name)
663        }
664    };
665
666    // Use let-binding pattern for non-opaque Named params
667    let use_let_bindings = generators::has_named_params(&func.params, opaque_types);
668    let call_args = if use_let_bindings {
669        generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
670    } else {
671        napi_gen_call_args(&func.params, opaque_types)
672    };
673
674    let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
675
676    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
677
678    let async_kw = if func.is_async { "async " } else { "" };
679
680    let body = if !can_delegate_fn {
681        // Try serde-based conversion for non-delegatable functions with Named params
682        if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
683            let serde_bindings =
684                generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, "    ");
685            let core_call = format!("{core_fn_path}({call_args})");
686
687            if matches!(func.return_type, TypeRef::Unit) {
688                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
689            } else {
690                let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
691                if wrapped == "val" {
692                    format!("{serde_bindings}{core_call}{err_conv}")
693                } else {
694                    format!("{serde_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
695                }
696            }
697        } else {
698            generators::gen_unimplemented_body(
699                &func.return_type,
700                &func.name,
701                func.error_type.is_some(),
702                cfg,
703                &func.params,
704            )
705        }
706    } else if func.is_async {
707        let core_call = format!("{core_fn_path}({call_args})");
708        let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref);
709        generators::gen_async_body(
710            &core_call,
711            cfg,
712            func.error_type.is_some(),
713            &return_wrap,
714            false,
715            "",
716            matches!(func.return_type, TypeRef::Unit),
717        )
718    } else {
719        let core_call = format!("{core_fn_path}({call_args})");
720        // Generate let bindings for Named params if needed
721        let let_bindings = if use_let_bindings {
722            generators::gen_named_let_bindings_pub(&func.params, opaque_types)
723        } else {
724            String::new()
725        };
726
727        if func.error_type.is_some() {
728            let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
729            if wrapped == "val" {
730                format!("{let_bindings}{core_call}{err_conv}")
731            } else {
732                format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
733            }
734        } else {
735            format!(
736                "{let_bindings}{}",
737                napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref)
738            )
739        }
740    };
741
742    let mut attrs = String::new();
743    // Per-item clippy suppression: too_many_arguments when >7 params
744    if func.params.len() > 7 {
745        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
746    }
747    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
748    if func.error_type.is_some() {
749        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
750    }
751    format!(
752        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
753         {body}\n}}",
754        func.name
755    )
756}
757
758/// NAPI-specific call args that casts i64 params to u64/usize where the core expects it.
759fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
760    params
761        .iter()
762        .map(|p| match &p.ty {
763            TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
764                let core_ty = core_prim_str(prim);
765                if p.optional {
766                    format!("{}.map(|v| v as {})", p.name, core_ty)
767                } else {
768                    format!("{} as {}", p.name, core_ty)
769                }
770            }
771            TypeRef::Duration => {
772                if p.optional {
773                    format!("{}.map(|v| std::time::Duration::from_secs(v as u64))", p.name)
774                } else {
775                    format!("std::time::Duration::from_secs({} as u64)", p.name)
776                }
777            }
778            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
779                if p.optional {
780                    format!("{}.as_ref().map(|v| &v.inner)", p.name)
781                } else {
782                    format!("&{}.inner", p.name)
783                }
784            }
785            TypeRef::Named(_) => {
786                if p.optional {
787                    format!("{}.map(Into::into)", p.name)
788                } else {
789                    format!("{}.into()", p.name)
790                }
791            }
792            TypeRef::String | TypeRef::Char => format!("&{}", p.name),
793            TypeRef::Path => format!("std::path::PathBuf::from({})", p.name),
794            TypeRef::Bytes => format!("&{}", p.name),
795            _ => p.name.clone(),
796        })
797        .collect::<Vec<_>>()
798        .join(", ")
799}
800
801/// NAPI-specific return wrapping for opaque instance methods.
802/// Extends the shared `wrap_return` with i64 casts for u64/usize/isize primitives.
803fn napi_wrap_return(
804    expr: &str,
805    return_type: &TypeRef,
806    type_name: &str,
807    opaque_types: &AHashSet<String>,
808    self_is_opaque: bool,
809    returns_ref: bool,
810) -> String {
811    match return_type {
812        TypeRef::Primitive(p) if needs_napi_cast(p) => {
813            format!("{expr} as i64")
814        }
815        TypeRef::Duration => format!("{expr}.as_secs() as i64"),
816        // Opaque Named returns need Js prefix
817        TypeRef::Named(n) if n == type_name && self_is_opaque => {
818            if returns_ref {
819                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
820            } else {
821                format!("Self {{ inner: Arc::new({expr}) }}")
822            }
823        }
824        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
825            if returns_ref {
826                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
827            } else {
828                format!("Js{n} {{ inner: Arc::new({expr}) }}")
829            }
830        }
831        TypeRef::Named(_) => {
832            if returns_ref {
833                format!("{expr}.clone().into()")
834            } else {
835                format!("{expr}.into()")
836            }
837        }
838        _ => generators::wrap_return(expr, return_type, type_name, opaque_types, self_is_opaque, returns_ref),
839    }
840}
841
842/// NAPI-specific return wrapping for free functions (no type_name context).
843fn napi_wrap_return_fn(
844    expr: &str,
845    return_type: &TypeRef,
846    opaque_types: &AHashSet<String>,
847    returns_ref: bool,
848) -> String {
849    match return_type {
850        TypeRef::Primitive(p) if needs_napi_cast(p) => {
851            format!("{expr} as i64")
852        }
853        TypeRef::Duration => format!("{expr}.as_secs() as i64"),
854        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
855            if returns_ref {
856                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
857            } else {
858                format!("Js{n} {{ inner: Arc::new({expr}) }}")
859            }
860        }
861        TypeRef::Named(_) => {
862            if returns_ref {
863                format!("{expr}.clone().into()")
864            } else {
865                format!("{expr}.into()")
866            }
867        }
868        TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
869            if returns_ref {
870                format!("{expr}.into()")
871            } else {
872                expr.to_string()
873            }
874        }
875        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
876        TypeRef::Json => format!("{expr}.to_string()"),
877        TypeRef::Optional(inner) => match inner.as_ref() {
878            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
879                if returns_ref {
880                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v.clone()) }})")
881                } else {
882                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v) }})")
883                }
884            }
885            TypeRef::Named(_) => {
886                if returns_ref {
887                    format!("{expr}.map(|v| v.clone().into())")
888                } else {
889                    format!("{expr}.map(Into::into)")
890                }
891            }
892            TypeRef::Path => {
893                format!("{expr}.map(Into::into)")
894            }
895            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
896                if returns_ref {
897                    format!("{expr}.map(Into::into)")
898                } else {
899                    expr.to_string()
900                }
901            }
902            _ => expr.to_string(),
903        },
904        TypeRef::Vec(inner) => match inner.as_ref() {
905            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
906                if returns_ref {
907                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v.clone()) }}).collect()")
908                } else {
909                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v) }}).collect()")
910                }
911            }
912            TypeRef::Named(_) => {
913                if returns_ref {
914                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
915                } else {
916                    format!("{expr}.into_iter().map(Into::into).collect()")
917                }
918            }
919            TypeRef::Path => {
920                format!("{expr}.into_iter().map(Into::into).collect()")
921            }
922            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
923                if returns_ref {
924                    format!("{expr}.into_iter().map(Into::into).collect()")
925                } else {
926                    expr.to_string()
927                }
928            }
929            _ => expr.to_string(),
930        },
931        _ => expr.to_string(),
932    }
933}
934
935fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
936    matches!(
937        p,
938        alef_core::ir::PrimitiveType::U64 | alef_core::ir::PrimitiveType::Usize | alef_core::ir::PrimitiveType::Isize
939    )
940}
941
942fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
943    match p {
944        alef_core::ir::PrimitiveType::U64 => "u64",
945        alef_core::ir::PrimitiveType::Usize => "usize",
946        alef_core::ir::PrimitiveType::Isize => "isize",
947        _ => unreachable!(),
948    }
949}
950
951/// Generate a global Tokio runtime for NAPI async support.
952fn gen_tokio_runtime() -> String {
953    "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
954    tokio::runtime::Builder::new_multi_thread()
955        .enable_all()
956        .build()
957        .expect(\"Failed to create Tokio runtime\")
958});"
959    .to_string()
960}