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            option_duration_on_defaults: true,
36        }
37    }
38}
39
40impl Backend for NapiBackend {
41    fn name(&self) -> &str {
42        "napi"
43    }
44
45    fn language(&self) -> Language {
46        Language::Node
47    }
48
49    fn capabilities(&self) -> Capabilities {
50        Capabilities {
51            supports_async: true,
52            supports_classes: true,
53            supports_enums: true,
54            supports_option: true,
55            supports_result: true,
56            ..Capabilities::default()
57        }
58    }
59
60    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
61        let mapper = NapiMapper;
62        let core_import = config.core_import();
63        let cfg = Self::binding_config(&core_import);
64
65        let mut builder = RustFileBuilder::new().with_generated_header();
66        builder.add_import("napi::*");
67        builder.add_import("napi_derive::napi");
68
69        // Import traits needed for trait method dispatch
70        for trait_path in generators::collect_trait_imports(api) {
71            builder.add_import(&trait_path);
72        }
73
74        // Only import HashMap when Map-typed fields or returns are present
75        let has_maps = api
76            .types
77            .iter()
78            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
79            || api
80                .functions
81                .iter()
82                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
83        if has_maps {
84            builder.add_import("std::collections::HashMap");
85        }
86
87        // Note: custom_modules for Node are TypeScript-only re-exports
88        // (used in generate_public_api), not Rust module declarations.
89
90        // Check if any function or method is async
91        let has_async =
92            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
93
94        if has_async {
95            builder.add_item(&gen_tokio_runtime());
96        }
97
98        // Check if we have opaque types and add Arc import if needed
99        let opaque_types: AHashSet<String> = api
100            .types
101            .iter()
102            .filter(|t| t.is_opaque)
103            .map(|t| t.name.clone())
104            .collect();
105        if !opaque_types.is_empty() {
106            builder.add_import("std::sync::Arc");
107        }
108
109        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
110        // and custom constructor. Use shared generators for enums and functions,
111        // but keep struct/method generation custom.
112        for typ in &api.types {
113            if typ.is_opaque {
114                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, "Js"));
115                builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &cfg, &opaque_types));
116            } else {
117                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
118                // napi(object) structs cannot have #[napi] impl blocks.
119                // gen_struct adds Default to derives when typ.has_default is true.
120                builder.add_item(&gen_struct(typ, &mapper));
121            }
122        }
123
124        for enum_def in &api.enums {
125            builder.add_item(&gen_enum(enum_def));
126        }
127
128        for func in &api.functions {
129            builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types));
130        }
131
132        let binding_to_core = alef_codegen::conversions::convertible_types(api);
133        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
134        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
135            type_name_prefix: "Js",
136            cast_large_ints_to_i64: true,
137            cast_f32_to_f64: true,
138            // optionalize_defaults: For types with has_default, conversion generators
139            // make all fields Option<T> and apply defaults via FromNapiValue,
140            // enabling JS users to pass partial objects and omit fields they want defaults for.
141            optionalize_defaults: true,
142            option_duration_on_defaults: true,
143            include_cfg_metadata: true,
144            ..Default::default()
145        };
146        // From/Into conversions using shared parameterized generators
147        for typ in &api.types {
148            if alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core) {
149                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
150                    typ,
151                    &core_import,
152                    &napi_conv_config,
153                ));
154            }
155            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
156                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
157                    typ,
158                    &core_import,
159                    &opaque_types,
160                    &napi_conv_config,
161                ));
162            }
163        }
164        for e in &api.enums {
165            let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
166            if is_tagged_data_enum {
167                // Tagged data enums use flattened struct — generate custom conversions
168                builder.add_item(&gen_tagged_enum_binding_to_core(e, &core_import));
169                builder.add_item(&gen_tagged_enum_core_to_binding(e, &core_import));
170            } else {
171                if alef_codegen::conversions::can_generate_enum_conversion(e) {
172                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
173                        e,
174                        &core_import,
175                        &napi_conv_config,
176                    ));
177                }
178                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
179                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
180                        e,
181                        &core_import,
182                        &napi_conv_config,
183                    ));
184                }
185            }
186        }
187
188        // Error types (variant name constants + converter functions)
189        for error in &api.errors {
190            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
191            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
192        }
193
194        // Build adapter body map (consumed by generators via body substitution)
195        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
196
197        let content = builder.build();
198
199        let output_dir = resolve_output_dir(
200            config.output.node.as_ref(),
201            &config.crate_config.name,
202            "crates/{name}-node/src/",
203        );
204
205        Ok(vec![GeneratedFile {
206            path: PathBuf::from(&output_dir).join("lib.rs"),
207            content,
208            generated_header: false,
209        }])
210    }
211
212    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
213        // Separate exports into functions (plain export) and types (export type)
214        let mut type_exports = vec![];
215        let mut function_exports = vec![];
216
217        // Collect all types (exported with Js prefix from native module) - export type
218        for typ in &api.types {
219            type_exports.push(format!("Js{}", typ.name));
220        }
221
222        // Collect all enums as value exports (runtime objects).
223        // NAPI generates const enum in .d.ts, but we post-process it to regular enum
224        // so they can be re-exported as values with verbatimModuleSyntax.
225        for enum_def in &api.enums {
226            function_exports.push(format!("Js{}", enum_def.name));
227        }
228
229        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
230        // Skip error types in the public API re-exports.
231
232        // Collect all functions (exported from native module) - plain export
233        for func in &api.functions {
234            // Convert snake_case to camelCase for JavaScript naming
235            let js_name = to_node_name(&func.name);
236            function_exports.push(js_name);
237        }
238
239        // Sort for consistent output
240        type_exports.sort();
241        function_exports.sort();
242
243        // Generate the index.ts re-export file using a single export block
244        // with inline `type` annotations for verbatimModuleSyntax compatibility.
245        let mut lines = vec![
246            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
247            "".to_string(),
248        ];
249
250        // Single export block: value exports (functions + enums) + inline type exports (structs)
251        let mut all_exports: Vec<String> = Vec::new();
252        for name in &function_exports {
253            all_exports.push(format!("  {name},"));
254        }
255        for name in &type_exports {
256            all_exports.push(format!("  type {name},"));
257        }
258        if !all_exports.is_empty() {
259            lines.push("export {".to_string());
260            lines.extend(all_exports);
261            lines.push(format!("}} from '{}';", config.node_package_name()));
262        }
263
264        // Append re-exports for custom modules (from [custom_modules] node = [...])
265        let custom_mods = config.custom_modules.for_language(Language::Node);
266        for module_name in custom_mods {
267            lines.push(format!("export * from './{module_name}';"));
268        }
269
270        let content = lines.join("\n");
271
272        // Output path: packages/typescript/src/index.ts
273        let output_path = PathBuf::from("packages/typescript/src/index.ts");
274
275        Ok(vec![GeneratedFile {
276            path: output_path,
277            content,
278            generated_header: false,
279        }])
280    }
281
282    fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
283        let content = gen_dts(api);
284
285        // `config.output.node` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
286        // `index.d.ts` belongs at the crate root, one level up from `src/`.
287        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
288        // Falls back to `crates/{name}-node/` if no node output is configured.
289        let src_dir = resolve_output_dir(
290            config.output.node.as_ref(),
291            &config.crate_config.name,
292            "crates/{name}-node/src/",
293        );
294        let crate_root = {
295            let p = PathBuf::from(&src_dir);
296            match p.file_name().and_then(|n| n.to_str()) {
297                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
298                _ => p,
299            }
300        };
301
302        Ok(vec![GeneratedFile {
303            path: crate_root.join("index.d.ts"),
304            content,
305            generated_header: false,
306        }])
307    }
308
309    fn build_config(&self) -> Option<BuildConfig> {
310        Some(BuildConfig {
311            tool: "napi",
312            crate_suffix: "-node",
313            depends_on_ffi: false,
314            post_build: vec![PostBuildStep::PatchFile {
315                path: "index.d.ts",
316                find: "export declare const enum",
317                replace: "export declare enum",
318            }],
319        })
320    }
321}
322
323/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
324fn gen_struct(typ: &TypeDef, mapper: &NapiMapper) -> String {
325    let mut struct_builder = StructBuilder::new(&format!("Js{}", typ.name));
326    // Use napi(object) so the struct can be used as function/method parameters (FromNapiValue)
327    struct_builder.add_attr("napi(object)");
328    struct_builder.add_derive("Clone");
329    // Types with has_default get #[derive(Default)] instead of a manual impl.
330    if typ.has_default {
331        struct_builder.add_derive("Default");
332    }
333
334    for field in &typ.fields {
335        let mapped_type = mapper.map_type(&field.ty);
336        // For types with Default, make all fields optional so JS callers
337        // can pass partial objects (missing fields get defaults).
338        let field_type = if field.optional || typ.has_default {
339            format!("Option<{}>", mapped_type)
340        } else {
341            mapped_type
342        };
343        let js_name = to_node_name(&field.name);
344        let attrs = if js_name != field.name {
345            vec![format!("napi(js_name = \"{}\")", js_name)]
346        } else {
347            vec![]
348        };
349        struct_builder.add_field(&field.name, &field_type, attrs);
350    }
351
352    struct_builder.build()
353}
354
355/// Generate NAPI methods for an opaque struct (delegates to self.inner).
356fn gen_opaque_struct_methods(
357    typ: &TypeDef,
358    mapper: &NapiMapper,
359    cfg: &RustBindingConfig,
360    opaque_types: &AHashSet<String>,
361) -> String {
362    let mut impl_builder = ImplBuilder::new(&format!("Js{}", typ.name));
363    impl_builder.add_attr("napi");
364
365    let (instance, statics) = partition_methods(&typ.methods);
366
367    for method in &instance {
368        impl_builder.add_method(&gen_opaque_instance_method(method, mapper, typ, cfg, opaque_types));
369    }
370    for method in &statics {
371        impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types));
372    }
373
374    impl_builder.build()
375}
376
377/// Generate an opaque instance method that delegates to self.inner.
378fn gen_opaque_instance_method(
379    method: &MethodDef,
380    mapper: &NapiMapper,
381    typ: &TypeDef,
382    cfg: &RustBindingConfig,
383    opaque_types: &AHashSet<String>,
384) -> String {
385    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
386    let return_type = mapper.map_type(&method.return_type);
387    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
388
389    let js_name = to_node_name(&method.name);
390    let js_name_attr = if js_name != method.name {
391        format!("(js_name = \"{}\")", js_name)
392    } else {
393        String::new()
394    };
395
396    let async_kw = if method.is_async { "async " } else { "" };
397
398    let type_name = &typ.name;
399    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
400    let call_args = napi_gen_call_args(&method.params, opaque_types);
401
402    // Use the shared can_auto_delegate check for opaque instance methods.
403    let opaque_can_delegate = !method.sanitized
404        && (!is_owned_receiver || typ.is_clone)
405        && method
406            .params
407            .iter()
408            .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
409        && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
410
411    let make_core_call = |method_name: &str| -> String {
412        if is_owned_receiver {
413            format!("(*self.inner).clone().{method_name}({call_args})")
414        } else {
415            format!("self.inner.{method_name}({call_args})")
416        }
417    };
418
419    let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
420
421    let async_result_wrap = napi_wrap_return(
422        "result",
423        &method.return_type,
424        type_name,
425        opaque_types,
426        true,
427        method.returns_ref,
428    );
429
430    let body = if !opaque_can_delegate {
431        // Try serde-based param conversion for methods with non-opaque Named params
432        if cfg.has_serde
433            && !method.sanitized
434            && generators::has_named_params(&method.params, opaque_types)
435            && method.error_type.is_some()
436            && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
437        {
438            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
439            let serde_bindings =
440                generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
441            let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
442            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
443            if matches!(method.return_type, TypeRef::Unit) {
444                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
445            } else {
446                let wrap = napi_wrap_return(
447                    "result",
448                    &method.return_type,
449                    type_name,
450                    opaque_types,
451                    true,
452                    method.returns_ref,
453                );
454                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
455            }
456        } else {
457            generators::gen_unimplemented_body(
458                &method.return_type,
459                &format!("{type_name}.{}", method.name),
460                method.error_type.is_some(),
461                cfg,
462                &method.params,
463            )
464        }
465    } else if method.is_async {
466        let inner_clone_line = "let inner = self.inner.clone();\n    ";
467        let core_call_str = make_async_core_call(&method.name);
468        generators::gen_async_body(
469            &core_call_str,
470            cfg,
471            method.error_type.is_some(),
472            &async_result_wrap,
473            true,
474            inner_clone_line,
475            matches!(method.return_type, TypeRef::Unit),
476        )
477    } else {
478        let core_call = make_core_call(&method.name);
479        if method.error_type.is_some() {
480            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
481            if matches!(method.return_type, TypeRef::Unit) {
482                format!("{core_call}{err_conv}?;\n    Ok(())")
483            } else {
484                let wrap = napi_wrap_return(
485                    "result",
486                    &method.return_type,
487                    type_name,
488                    opaque_types,
489                    true,
490                    method.returns_ref,
491                );
492                format!("let result = {core_call}{err_conv}?;\n    Ok({wrap})")
493            }
494        } else {
495            napi_wrap_return(
496                &core_call,
497                &method.return_type,
498                type_name,
499                opaque_types,
500                true,
501                method.returns_ref,
502            )
503        }
504    };
505
506    let mut attrs = String::new();
507    // Per-item clippy suppression: too_many_arguments when >7 params (including &self)
508    if method.params.len() + 1 > 7 {
509        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
510    }
511    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
512    if method.error_type.is_some() {
513        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
514    }
515    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
516    if generators::is_trait_method_name(&method.name) {
517        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
518    }
519    format!(
520        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n    \
521         {body}\n}}",
522        method.name
523    )
524}
525
526/// Generate a static method binding.
527fn gen_static_method(
528    method: &MethodDef,
529    mapper: &NapiMapper,
530    typ: &TypeDef,
531    cfg: &RustBindingConfig,
532    opaque_types: &AHashSet<String>,
533) -> String {
534    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
535    let return_type = mapper.map_type(&method.return_type);
536    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
537
538    let js_name = to_node_name(&method.name);
539    let js_name_attr = if js_name != method.name {
540        format!("(js_name = \"{}\")", js_name)
541    } else {
542        String::new()
543    };
544
545    let type_name = &typ.name;
546    let core_type_path = typ.rust_path.replace('-', "_");
547    let call_args = napi_gen_call_args(&method.params, opaque_types);
548    let can_delegate_static = can_auto_delegate(method, opaque_types);
549
550    let async_kw = if method.is_async { "async " } else { "" };
551
552    let body = if !can_delegate_static {
553        generators::gen_unimplemented_body(
554            &method.return_type,
555            &format!("{type_name}::{}", method.name),
556            method.error_type.is_some(),
557            cfg,
558            &method.params,
559        )
560    } else if method.is_async {
561        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
562        let return_wrap = napi_wrap_return(
563            "result",
564            &method.return_type,
565            type_name,
566            opaque_types,
567            typ.is_opaque,
568            method.returns_ref,
569        );
570        generators::gen_async_body(
571            &core_call,
572            cfg,
573            method.error_type.is_some(),
574            &return_wrap,
575            false,
576            "",
577            matches!(method.return_type, TypeRef::Unit),
578        )
579    } else {
580        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
581        if method.error_type.is_some() {
582            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
583            let wrapped = napi_wrap_return(
584                "val",
585                &method.return_type,
586                type_name,
587                opaque_types,
588                typ.is_opaque,
589                method.returns_ref,
590            );
591            if wrapped == "val" {
592                format!("{core_call}{err_conv}")
593            } else {
594                format!("{core_call}.map(|val| {wrapped}){err_conv}")
595            }
596        } else {
597            napi_wrap_return(
598                &core_call,
599                &method.return_type,
600                type_name,
601                opaque_types,
602                typ.is_opaque,
603                method.returns_ref,
604            )
605        }
606    };
607
608    let mut attrs = String::new();
609    // Per-item clippy suppression: too_many_arguments when >7 params
610    if method.params.len() > 7 {
611        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
612    }
613    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
614    if method.error_type.is_some() {
615        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
616    }
617    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
618    if generators::is_trait_method_name(&method.name) {
619        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
620    }
621    format!(
622        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
623         {body}\n}}",
624        method.name
625    )
626}
627
628/// Generate a NAPI enum definition using string_enum with Js prefix.
629/// Generate a NAPI enum definition.
630/// For simple enums (no variant fields): generates `#[napi(string_enum)]`.
631/// For tagged enums with data fields: generates a flattened `#[napi(object)]` struct
632/// with a discriminant field and all variant fields as optional.
633fn gen_enum(enum_def: &EnumDef) -> String {
634    let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
635
636    if is_tagged_data_enum {
637        return gen_tagged_enum_as_object(enum_def);
638    }
639
640    // Simple string enum
641    let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
642        "snake_case" => Some("snake_case"),
643        "camelCase" => Some("camelCase"),
644        "kebab-case" => Some("kebab-case"),
645        "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
646        "lowercase" => Some("lowercase"),
647        "UPPERCASE" => Some("UPPERCASE"),
648        "PascalCase" => Some("PascalCase"),
649        _ => None,
650    });
651
652    let string_enum_attr = match napi_case {
653        Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
654        None => "#[napi(string_enum)]".to_string(),
655    };
656
657    let mut lines = vec![
658        string_enum_attr,
659        "#[derive(Clone)]".to_string(),
660        format!("pub enum Js{} {{", enum_def.name),
661    ];
662
663    for variant in &enum_def.variants {
664        lines.push(format!("    {},", variant.name));
665    }
666
667    lines.push("}".to_string());
668
669    // Default impl for config constructor unwrap_or_default()
670    if let Some(first) = enum_def.variants.first() {
671        lines.push(String::new());
672        lines.push("#[allow(clippy::derivable_impls)]".to_string());
673        lines.push(format!("impl Default for Js{} {{", enum_def.name));
674        lines.push(format!("    fn default() -> Self {{ Self::{} }}", first.name));
675        lines.push("}".to_string());
676    }
677
678    lines.join("\n")
679}
680
681/// Generate a tagged enum as a flattened `#[napi(object)]` struct.
682/// E.g. `AuthConfig { Basic { username, password }, Bearer { token } }` becomes:
683/// ```rust,ignore
684/// #[napi(object)]
685/// struct JsAuthConfig {
686///     #[napi(js_name = "type")]
687///     pub auth_type: String,
688///     pub username: Option<String>,
689///     pub password: Option<String>,
690///     pub token: Option<String>,
691/// }
692/// ```
693fn gen_tagged_enum_as_object(enum_def: &EnumDef) -> String {
694    use alef_codegen::type_mapper::TypeMapper;
695    let mapper = NapiMapper;
696
697    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
698
699    let mut lines = vec![
700        "#[derive(Clone)]".to_string(),
701        "#[napi(object)]".to_string(),
702        format!("pub struct Js{} {{", enum_def.name),
703        format!("    #[napi(js_name = \"{tag_field}\")]"),
704        format!("    pub {tag_field}_tag: String,"),
705    ];
706
707    // Collect all unique fields across all variants (all made optional)
708    let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
709    for variant in &enum_def.variants {
710        for field in &variant.fields {
711            if seen_fields.insert(field.name.clone()) {
712                let field_type = mapper.map_type(&field.ty);
713                let js_name = alef_codegen::naming::to_node_name(&field.name);
714                if js_name != field.name {
715                    lines.push(format!("    #[napi(js_name = \"{js_name}\")]"));
716                }
717                lines.push(format!("    pub {}: Option<{field_type}>,", field.name));
718            }
719        }
720    }
721
722    lines.push("}".to_string());
723
724    // Default impl
725    lines.push(String::new());
726    lines.push("#[allow(clippy::derivable_impls)]".to_string());
727    lines.push(format!("impl Default for Js{} {{", enum_def.name));
728    lines.push(format!(
729        "    fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
730        seen_fields
731            .iter()
732            .map(|f| format!("{f}: None"))
733            .collect::<Vec<_>>()
734            .join(", ")
735    ));
736    lines.push("}".to_string());
737
738    lines.join("\n")
739}
740
741/// Generate a free function binding.
742fn gen_function(
743    func: &FunctionDef,
744    mapper: &NapiMapper,
745    cfg: &RustBindingConfig,
746    opaque_types: &AHashSet<String>,
747) -> String {
748    let params = function_params(&func.params, &|ty| {
749        // Opaque Named params must be received by reference since NAPI opaque
750        // structs don't implement FromNapiValue (they use Arc<T> internally).
751        if let TypeRef::Named(n) = ty {
752            if opaque_types.contains(n.as_str()) {
753                return format!("&Js{n}");
754            }
755        }
756        mapper.map_type(ty)
757    });
758    let return_type = mapper.map_type(&func.return_type);
759    let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
760
761    let js_name = to_node_name(&func.name);
762    let js_name_attr = if js_name != func.name {
763        format!("(js_name = \"{}\")", js_name)
764    } else {
765        String::new()
766    };
767
768    let core_import = cfg.core_import;
769    let core_fn_path = {
770        let path = func.rust_path.replace('-', "_");
771        if path.starts_with(core_import) {
772            path
773        } else {
774            format!("{core_import}::{}", func.name)
775        }
776    };
777
778    // Use let-binding pattern for non-opaque Named params
779    let use_let_bindings = generators::has_named_params(&func.params, opaque_types);
780    let call_args = if use_let_bindings {
781        generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
782    } else {
783        napi_gen_call_args(&func.params, opaque_types)
784    };
785
786    let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
787
788    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
789
790    let async_kw = if func.is_async { "async " } else { "" };
791
792    let body = if !can_delegate_fn {
793        // Try serde-based conversion for non-delegatable functions with Named params
794        if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
795            let serde_bindings =
796                generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, "    ");
797            let core_call = format!("{core_fn_path}({call_args})");
798
799            if matches!(func.return_type, TypeRef::Unit) {
800                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
801            } else {
802                let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
803                if wrapped == "val" {
804                    format!("{serde_bindings}{core_call}{err_conv}")
805                } else {
806                    format!("{serde_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
807                }
808            }
809        } else {
810            generators::gen_unimplemented_body(
811                &func.return_type,
812                &func.name,
813                func.error_type.is_some(),
814                cfg,
815                &func.params,
816            )
817        }
818    } else if func.is_async {
819        let core_call = format!("{core_fn_path}({call_args})");
820        let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref);
821        generators::gen_async_body(
822            &core_call,
823            cfg,
824            func.error_type.is_some(),
825            &return_wrap,
826            false,
827            "",
828            matches!(func.return_type, TypeRef::Unit),
829        )
830    } else {
831        let core_call = format!("{core_fn_path}({call_args})");
832        // Generate let bindings for Named params if needed
833        let let_bindings = if use_let_bindings {
834            generators::gen_named_let_bindings_pub(&func.params, opaque_types)
835        } else {
836            String::new()
837        };
838
839        if func.error_type.is_some() {
840            let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
841            if wrapped == "val" {
842                format!("{let_bindings}{core_call}{err_conv}")
843            } else {
844                format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
845            }
846        } else {
847            format!(
848                "{let_bindings}{}",
849                napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref)
850            )
851        }
852    };
853
854    let mut attrs = String::new();
855    // Per-item clippy suppression: too_many_arguments when >7 params
856    if func.params.len() > 7 {
857        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
858    }
859    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
860    if func.error_type.is_some() {
861        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
862    }
863    format!(
864        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
865         {body}\n}}",
866        func.name
867    )
868}
869
870/// NAPI-specific call args that casts i64 params to u64/usize where the core expects it.
871fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
872    params
873        .iter()
874        .map(|p| match &p.ty {
875            TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
876                let core_ty = core_prim_str(prim);
877                if p.optional {
878                    format!("{}.map(|v| v as {})", p.name, core_ty)
879                } else {
880                    format!("{} as {}", p.name, core_ty)
881                }
882            }
883            TypeRef::Duration => {
884                if p.optional {
885                    format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
886                } else {
887                    format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
888                }
889            }
890            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
891                if p.optional {
892                    format!("{}.as_ref().map(|v| &v.inner)", p.name)
893                } else {
894                    format!("&{}.inner", p.name)
895                }
896            }
897            TypeRef::Named(_) => {
898                if p.optional {
899                    format!("{}.map(Into::into)", p.name)
900                } else {
901                    format!("{}.into()", p.name)
902                }
903            }
904            TypeRef::String | TypeRef::Char => format!("&{}", p.name),
905            TypeRef::Path => format!("std::path::PathBuf::from({})", p.name),
906            TypeRef::Bytes => format!("&{}", p.name),
907            _ => p.name.clone(),
908        })
909        .collect::<Vec<_>>()
910        .join(", ")
911}
912
913/// NAPI-specific return wrapping for opaque instance methods.
914/// Extends the shared `wrap_return` with i64 casts for u64/usize/isize primitives.
915fn napi_wrap_return(
916    expr: &str,
917    return_type: &TypeRef,
918    type_name: &str,
919    opaque_types: &AHashSet<String>,
920    self_is_opaque: bool,
921    returns_ref: bool,
922) -> String {
923    match return_type {
924        TypeRef::Primitive(p) if needs_napi_cast(p) => {
925            format!("{expr} as i64")
926        }
927        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
928        // Opaque Named returns need Js prefix
929        TypeRef::Named(n) if n == type_name && self_is_opaque => {
930            if returns_ref {
931                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
932            } else {
933                format!("Self {{ inner: Arc::new({expr}) }}")
934            }
935        }
936        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
937            if returns_ref {
938                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
939            } else {
940                format!("Js{n} {{ inner: Arc::new({expr}) }}")
941            }
942        }
943        TypeRef::Named(_) => {
944            if returns_ref {
945                format!("{expr}.clone().into()")
946            } else {
947                format!("{expr}.into()")
948            }
949        }
950        _ => generators::wrap_return(
951            expr,
952            return_type,
953            type_name,
954            opaque_types,
955            self_is_opaque,
956            returns_ref,
957            false,
958        ),
959    }
960}
961
962/// NAPI-specific return wrapping for free functions (no type_name context).
963fn napi_wrap_return_fn(
964    expr: &str,
965    return_type: &TypeRef,
966    opaque_types: &AHashSet<String>,
967    returns_ref: bool,
968) -> String {
969    match return_type {
970        TypeRef::Primitive(p) if needs_napi_cast(p) => {
971            format!("{expr} as i64")
972        }
973        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
974        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
975            if returns_ref {
976                format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
977            } else {
978                format!("Js{n} {{ inner: Arc::new({expr}) }}")
979            }
980        }
981        TypeRef::Named(_) => {
982            if returns_ref {
983                format!("{expr}.clone().into()")
984            } else {
985                format!("{expr}.into()")
986            }
987        }
988        TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
989            if returns_ref {
990                format!("{expr}.into()")
991            } else {
992                expr.to_string()
993            }
994        }
995        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
996        TypeRef::Json => format!("{expr}.to_string()"),
997        TypeRef::Optional(inner) => match inner.as_ref() {
998            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
999                if returns_ref {
1000                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v.clone()) }})")
1001                } else {
1002                    format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v) }})")
1003                }
1004            }
1005            TypeRef::Named(_) => {
1006                if returns_ref {
1007                    format!("{expr}.map(|v| v.clone().into())")
1008                } else {
1009                    format!("{expr}.map(Into::into)")
1010                }
1011            }
1012            TypeRef::Path => {
1013                format!("{expr}.map(Into::into)")
1014            }
1015            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1016                if returns_ref {
1017                    format!("{expr}.map(Into::into)")
1018                } else {
1019                    expr.to_string()
1020                }
1021            }
1022            _ => expr.to_string(),
1023        },
1024        TypeRef::Vec(inner) => match inner.as_ref() {
1025            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1026                if returns_ref {
1027                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1028                } else {
1029                    format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v) }}).collect()")
1030                }
1031            }
1032            TypeRef::Named(_) => {
1033                if returns_ref {
1034                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1035                } else {
1036                    format!("{expr}.into_iter().map(Into::into).collect()")
1037                }
1038            }
1039            TypeRef::Path => {
1040                format!("{expr}.into_iter().map(Into::into).collect()")
1041            }
1042            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1043                if returns_ref {
1044                    format!("{expr}.into_iter().map(Into::into).collect()")
1045                } else {
1046                    expr.to_string()
1047                }
1048            }
1049            _ => expr.to_string(),
1050        },
1051        _ => expr.to_string(),
1052    }
1053}
1054
1055fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1056    matches!(
1057        p,
1058        alef_core::ir::PrimitiveType::U64 | alef_core::ir::PrimitiveType::Usize | alef_core::ir::PrimitiveType::Isize
1059    )
1060}
1061
1062fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1063    match p {
1064        alef_core::ir::PrimitiveType::U64 => "u64",
1065        alef_core::ir::PrimitiveType::Usize => "usize",
1066        alef_core::ir::PrimitiveType::Isize => "isize",
1067        _ => unreachable!(),
1068    }
1069}
1070
1071/// Generate a global Tokio runtime for NAPI async support.
1072fn gen_tokio_runtime() -> String {
1073    "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1074    tokio::runtime::Builder::new_multi_thread()
1075        .enable_all()
1076        .build()
1077        .expect(\"Failed to create Tokio runtime\")
1078});"
1079    .to_string()
1080}
1081
1082/// Generate an `index.d.ts` file for the NAPI binding crate.
1083///
1084/// NAPI-RS generates `const enum` in its auto-generated `.d.ts`, which is incompatible
1085/// with `verbatimModuleSyntax` (const enums cannot be re-exported as values). This
1086/// function produces an equivalent `.d.ts` with `export declare enum` (regular enum)
1087/// so the file can be committed and used directly without a post-build patch step.
1088///
1089/// The output format matches what NAPI-RS would generate after patching, using the same
1090/// alphabetical ordering and type declarations seen in the committed `index.d.ts` files.
1091fn gen_dts(api: &ApiSurface) -> String {
1092    let mut lines: Vec<String> = vec![
1093        "/* auto-generated by alef */".to_string(),
1094        "/* eslint-disable */".to_string(),
1095    ];
1096
1097    // Collect all declarations: opaque types (classes), plain structs (interfaces), enums, functions.
1098    // Sort each group alphabetically to produce stable, deterministic output.
1099
1100    // Opaque types → `export declare class`
1101    let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1102    opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1103
1104    // Plain structs → `export interface`
1105    let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1106    plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1107
1108    // Enums → `export declare enum`
1109    let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1110    sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1111
1112    // Functions → `export declare function`
1113    let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1114    sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1115
1116    // Build a merged list of all declarations sorted by their Js-prefixed name so the
1117    // output is fully alphabetical (matching the committed index.d.ts format).
1118    enum Decl<'a> {
1119        Class(&'a TypeDef),
1120        Interface(&'a TypeDef),
1121        Enum(&'a EnumDef),
1122        Function(&'a FunctionDef),
1123    }
1124
1125    let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1126    for t in &opaque_types {
1127        all_decls.push((format!("Js{}", t.name), Decl::Class(t)));
1128    }
1129    for t in &plain_types {
1130        all_decls.push((format!("Js{}", t.name), Decl::Interface(t)));
1131    }
1132    for e in &sorted_enums {
1133        all_decls.push((format!("Js{}", e.name), Decl::Enum(e)));
1134    }
1135    for f in &sorted_fns {
1136        all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1137    }
1138    all_decls.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
1139
1140    for (_, decl) in &all_decls {
1141        lines.push(String::new());
1142        match decl {
1143            Decl::Class(typ) => {
1144                lines.push(format!("export declare class Js{} {{", typ.name));
1145                for method in &typ.methods {
1146                    let js_name = to_node_name(&method.name);
1147                    let params = dts_params(&method.params);
1148                    let ret = dts_return_type(&method.return_type, method.error_type.is_some(), method.is_async);
1149                    if method.is_static {
1150                        lines.push(format!("  static {js_name}({params}): {ret}"));
1151                    } else {
1152                        lines.push(format!("  {js_name}({params}): {ret}"));
1153                    }
1154                }
1155                lines.push("}".to_string());
1156            }
1157            Decl::Interface(typ) => {
1158                lines.push(format!("export interface Js{} {{", typ.name));
1159                for field in &typ.fields {
1160                    let js_name = to_node_name(&field.name);
1161                    let ts_ty = dts_type(&field.ty);
1162                    // All fields on plain structs are optional (NAPI napi(object) makes them Option).
1163                    lines.push(format!("  {js_name}?: {ts_ty}"));
1164                }
1165                lines.push("}".to_string());
1166            }
1167            Decl::Enum(e) => {
1168                lines.push(format!("export declare enum Js{} {{", e.name));
1169                for variant in &e.variants {
1170                    // NAPI string_enum: variant values are the variant name as a string literal.
1171                    let value = variant.serde_rename.as_deref().unwrap_or(variant.name.as_str());
1172                    lines.push(format!("  {} = '{}',", variant.name, value));
1173                }
1174                lines.push("}".to_string());
1175            }
1176            Decl::Function(func) => {
1177                let js_name = to_node_name(&func.name);
1178                let params = dts_params(&func.params);
1179                let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async);
1180                lines.push(format!("export declare function {js_name}({params}): {ret}"));
1181            }
1182        }
1183    }
1184
1185    lines.push(String::new());
1186    lines.join("\n")
1187}
1188
1189/// Map an IR `TypeRef` to its TypeScript equivalent for `.d.ts` generation.
1190fn dts_type(ty: &TypeRef) -> String {
1191    match ty {
1192        TypeRef::Primitive(p) => match p {
1193            alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1194            alef_core::ir::PrimitiveType::U8
1195            | alef_core::ir::PrimitiveType::U16
1196            | alef_core::ir::PrimitiveType::U32
1197            | alef_core::ir::PrimitiveType::I8
1198            | alef_core::ir::PrimitiveType::I16
1199            | alef_core::ir::PrimitiveType::I32
1200            | alef_core::ir::PrimitiveType::F32
1201            | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1202            // NAPI maps u64/usize/isize to i64 on the Rust side; JS sees it as number.
1203            alef_core::ir::PrimitiveType::U64
1204            | alef_core::ir::PrimitiveType::I64
1205            | alef_core::ir::PrimitiveType::Usize
1206            | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1207        },
1208        TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1209        TypeRef::Bytes => "Uint8Array".to_string(),
1210        TypeRef::Json => "string".to_string(),
1211        TypeRef::Duration => "number".to_string(),
1212        TypeRef::Unit => "void".to_string(),
1213        TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner)),
1214        TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner)),
1215        TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k), dts_type(v)),
1216        TypeRef::Named(name) => format!("Js{name}"),
1217    }
1218}
1219
1220/// Render a list of parameters as a TypeScript parameter string for `.d.ts`.
1221fn dts_params(params: &[ParamDef]) -> String {
1222    params
1223        .iter()
1224        .map(|p| {
1225            let js_name = to_node_name(&p.name);
1226            let ts_ty = dts_type(&p.ty);
1227            if p.optional {
1228                format!("{js_name}?: {ts_ty} | undefined | null")
1229            } else {
1230                format!("{js_name}: {ts_ty}")
1231            }
1232        })
1233        .collect::<Vec<_>>()
1234        .join(", ")
1235}
1236
1237/// Render the TypeScript return type for a function/method in `.d.ts`.
1238///
1239/// Async functions return `Promise<T>`. Functions that can error still return `T`
1240/// (NAPI throws JS exceptions on error, so the `.d.ts` signature just shows the success type).
1241fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool) -> String {
1242    let base = match ret {
1243        TypeRef::Unit => "void".to_string(),
1244        other => dts_type(other),
1245    };
1246    if is_async { format!("Promise<{base}>") } else { base }
1247}
1248
1249/// Generate `From<JsTaggedEnum> for core::TaggedEnum` for a flattened struct representation.
1250fn gen_tagged_enum_binding_to_core(enum_def: &EnumDef, core_import: &str) -> String {
1251    use alef_core::ir::TypeRef;
1252    use std::fmt::Write;
1253    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1254    let binding_name = format!("Js{}", enum_def.name);
1255    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1256
1257    let mut out = String::with_capacity(512);
1258    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1259    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
1260    writeln!(out, "        match val.{tag_field}_tag.as_str() {{").ok();
1261
1262    for variant in &enum_def.variants {
1263        let default_tag = variant.name.to_lowercase();
1264        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1265        if variant.fields.is_empty() {
1266            writeln!(out, "            \"{tag_value}\" => Self::{},", variant.name).ok();
1267        } else {
1268            let field_inits: Vec<String> = variant
1269                .fields
1270                .iter()
1271                .map(|f| {
1272                    if f.optional {
1273                        // Core field is Option<T>; binding also has Option<T> — pass through.
1274                        format!("{}: val.{}", f.name, f.name)
1275                    } else if f.sanitized {
1276                        // Sanitized fields can't be faithfully round-tripped; use Default.
1277                        format!("{}: Default::default()", f.name)
1278                    } else {
1279                        match &f.ty {
1280                            TypeRef::Named(_) => {
1281                                // Named types need .into() to convert Js* → core type.
1282                                format!("{}: val.{}.unwrap_or_default().into()", f.name, f.name)
1283                            }
1284                            _ => {
1285                                format!("{}: val.{}.unwrap_or_default()", f.name, f.name)
1286                            }
1287                        }
1288                    }
1289                })
1290                .collect();
1291            writeln!(
1292                out,
1293                "            \"{tag_value}\" => Self::{} {{ {} }},",
1294                variant.name,
1295                field_inits.join(", ")
1296            )
1297            .ok();
1298        }
1299    }
1300
1301    // Default fallback to first variant
1302    if let Some(first) = enum_def.variants.first() {
1303        if first.fields.is_empty() {
1304            writeln!(out, "            _ => Self::{},", first.name).ok();
1305        } else {
1306            let defaults: Vec<String> = first
1307                .fields
1308                .iter()
1309                .map(|f| format!("{}: Default::default()", f.name))
1310                .collect();
1311            writeln!(
1312                out,
1313                "            _ => Self::{} {{ {} }},",
1314                first.name,
1315                defaults.join(", ")
1316            )
1317            .ok();
1318        }
1319    }
1320
1321    writeln!(out, "        }}").ok();
1322    writeln!(out, "    }}").ok();
1323    write!(out, "}}").ok();
1324    out
1325}
1326
1327/// Generate `From<core::TaggedEnum> for JsTaggedEnum` for a flattened struct representation.
1328fn gen_tagged_enum_core_to_binding(enum_def: &EnumDef, core_import: &str) -> String {
1329    use std::fmt::Write;
1330    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1331    let binding_name = format!("Js{}", enum_def.name);
1332    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1333
1334    // Collect all field names across all variants
1335    let all_fields: Vec<String> = {
1336        let mut fields = std::collections::BTreeSet::new();
1337        for v in &enum_def.variants {
1338            for f in &v.fields {
1339                fields.insert(f.name.clone());
1340            }
1341        }
1342        fields.into_iter().collect()
1343    };
1344
1345    let mut out = String::with_capacity(512);
1346    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1347    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
1348    writeln!(out, "        match val {{").ok();
1349
1350    for variant in &enum_def.variants {
1351        let default_tag = variant.name.to_lowercase();
1352        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1353        let _variant_field_names: std::collections::BTreeSet<String> =
1354            variant.fields.iter().map(|f| f.name.clone()).collect();
1355
1356        if variant.fields.is_empty() {
1357            writeln!(
1358                out,
1359                "            {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1360                variant.name,
1361                all_fields
1362                    .iter()
1363                    .map(|f| format!("{f}: None"))
1364                    .collect::<Vec<_>>()
1365                    .join(", ")
1366            )
1367            .ok();
1368        } else {
1369            use alef_core::ir::TypeRef;
1370            let destructured: Vec<String> = variant.fields.iter().map(|f| f.name.clone()).collect();
1371            // Build a lookup map so we can access field metadata when generating inits.
1372            let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
1373                variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1374            let field_inits: Vec<String> = all_fields
1375                .iter()
1376                .map(|f| {
1377                    if let Some(field) = variant_field_map.get(f.as_str()) {
1378                        if field.optional {
1379                            // Core field is already Option<T>; destructured variable is Option<T>.
1380                            // Pass through directly — no Some() wrapping needed.
1381                            format!("{f}: {f}")
1382                        } else if field.sanitized {
1383                            // Sanitized fields can't be faithfully converted to binding.
1384                            // Use None to signal absence (lossy but type-correct).
1385                            format!("{f}: None")
1386                        } else {
1387                            match &field.ty {
1388                                TypeRef::Named(_) => {
1389                                    // Named types need .into() to convert core → Js* type.
1390                                    format!("{f}: Some({f}.into())")
1391                                }
1392                                _ => {
1393                                    format!("{f}: Some({f})")
1394                                }
1395                            }
1396                        }
1397                    } else {
1398                        format!("{f}: None")
1399                    }
1400                })
1401                .collect();
1402            writeln!(
1403                out,
1404                "            {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1405                variant.name,
1406                destructured.join(", "),
1407                field_inits.join(", ")
1408            )
1409            .ok();
1410        }
1411    }
1412
1413    writeln!(out, "        }}").ok();
1414    writeln!(out, "    }}").ok();
1415    write!(out, "}}").ok();
1416    out
1417}