Skip to main content

alef_backend_napi/gen_bindings/
mod.rs

1//! NAPI-RS (Node.js) backend: orchestration and `Backend` trait implementation.
2
3pub mod capsule;
4pub mod enums;
5pub mod errors;
6pub mod functions;
7pub mod methods;
8pub mod types;
9
10use crate::type_map::NapiMapper;
11use ahash::AHashSet;
12use alef_codegen::builder::RustFileBuilder;
13use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
14use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
15use alef_core::config::{Language, NodeCapsuleTypeConfig, ResolvedCrateConfig, resolve_output_dir};
16use alef_core::ir::{ApiSurface, TypeRef};
17use std::collections::HashMap;
18use std::path::PathBuf;
19
20pub struct NapiBackend;
21
22impl NapiBackend {
23    fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
24        RustBindingConfig {
25            struct_attrs: &["napi"],
26            field_attrs: &[],
27            struct_derives: &["Clone"],
28            method_block_attr: Some("napi"),
29            constructor_attr: "#[napi(constructor)]",
30            static_attr: None,
31            function_attr: "#[napi]",
32            enum_attrs: &["napi(string_enum)"],
33            enum_derives: &["Clone"],
34            needs_signature: false,
35            signature_prefix: "",
36            signature_suffix: "",
37            core_import,
38            async_pattern: AsyncPattern::NapiNativeAsync,
39            has_serde,
40            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
41            type_name_prefix: prefix,
42            option_duration_on_defaults: true,
43            opaque_type_names: &[],
44            skip_impl_constructor: false,
45            cast_uints_to_i32: false,
46            cast_large_ints_to_f64: false,
47            named_non_opaque_params_by_ref: false,
48            lossy_skip_types: &[],
49            serializable_opaque_type_names: &[],
50            never_skip_cfg_field_names: &[],
51        }
52    }
53}
54
55impl Backend for NapiBackend {
56    fn name(&self) -> &str {
57        "napi"
58    }
59
60    fn language(&self) -> Language {
61        Language::Node
62    }
63
64    fn capabilities(&self) -> Capabilities {
65        Capabilities {
66            supports_async: true,
67            supports_classes: true,
68            supports_enums: true,
69            supports_option: true,
70            supports_result: true,
71            ..Capabilities::default()
72        }
73    }
74
75    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
76        let prefix = config.node_type_prefix();
77        let trait_type_names: AHashSet<String> = api
78            .types
79            .iter()
80            .filter(|t| t.is_trait)
81            .map(|t| t.name.clone())
82            .collect();
83        let capsule_type_names_for_mapper: AHashSet<String> = config
84            .node
85            .as_ref()
86            .map(|c| c.capsule_types.keys().cloned().collect())
87            .unwrap_or_default();
88        let mapper =
89            NapiMapper::with_traits_and_capsules(prefix.clone(), trait_type_names, capsule_type_names_for_mapper);
90        let core_import = config.core_import_name();
91
92        // Detect serde availability from the output crate's Cargo.toml
93        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
94        let has_serde = alef_core::config::detect_serde_available(&output_dir);
95        let mut cfg = Self::binding_config(&core_import, &prefix, has_serde);
96        let never_skip_cfg_field_names: Vec<String> = config
97            .trait_bridges
98            .iter()
99            .filter_map(|b| {
100                if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
101                    b.resolved_options_field().map(|s| s.to_string())
102                } else {
103                    None
104                }
105            })
106            .collect();
107        cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
108
109        let mut builder = RustFileBuilder::new().with_generated_header();
110        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
111        builder.add_inner_attribute("allow(unsafe_code)");
112        builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait)");
113        // Cast lints fire heavily on the JS u32/i64/Number bridge — these are
114        // intentional, deliberate at the FFI boundary. Pedantic/nursery noise
115        // (must_use_candidate, use_self, missing_const_for_fn, etc.) is
116        // suppressed for the same reasons documented in the pyo3 backend.
117        builder.add_inner_attribute(
118            "allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::default_trait_access, clippy::useless_conversion, clippy::unsafe_derive_deserialize, clippy::must_use_candidate, clippy::return_self_not_must_use, clippy::use_self, clippy::missing_const_for_fn, clippy::missing_errors_doc, clippy::needless_pass_by_value, clippy::doc_markdown, clippy::derive_partial_eq_without_eq, clippy::uninlined_format_args, clippy::redundant_clone, clippy::implicit_clone, clippy::redundant_closure_for_method_calls, clippy::wildcard_imports, clippy::option_if_let_else, clippy::too_many_lines)",
119        );
120        builder.add_import("napi::*");
121        builder.add_import("napi_derive::napi");
122
123        // Always import serde_json for type conversion in From/Into impls,
124        // even if the binding crate doesn't explicitly list it as a dependency.
125        // serde_json is needed for conversions of types with serde-serializable fields.
126        builder.add_import("serde_json");
127
128        // Import traits needed for trait method dispatch
129        for trait_path in generators::collect_trait_imports(api) {
130            builder.add_import(&trait_path);
131        }
132
133        // Only import HashMap when Map-typed fields or returns are present
134        let has_maps = api
135            .types
136            .iter()
137            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
138            || api
139                .functions
140                .iter()
141                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
142        if has_maps {
143            builder.add_import("std::collections::HashMap");
144        }
145
146        // Check if any function or method is async
147        let has_async =
148            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
149
150        if has_async {
151            builder.add_item(&functions::gen_tokio_runtime());
152        }
153
154        // Extract capsule_types from NodeConfig. Types listed here skip #[napi] opaque-class
155        // emission; functions returning them produce a JsObject with __parser External<T>.
156        let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
157            .node
158            .as_ref()
159            .map(|c| c.capsule_types.clone())
160            .unwrap_or_default();
161
162        // When capsule types are present, generated shims call set_named_property which
163        // requires the JsObjectValue trait to be in scope.
164        if !capsule_types.is_empty() {
165            builder.add_import("napi::bindgen_prelude::JsObjectValue");
166            // Emit the FFI declarations for napi_create_external and napi_type_tag_object,
167            // and any per-capsule type tag constants. Done once per crate.
168            builder.add_item(&capsule::gen_ffi_declarations());
169            let constants = capsule::gen_type_tag_constants(&capsule_types);
170            if !constants.is_empty() {
171                builder.add_item(&constants);
172            }
173        }
174
175        // Check if we have opaque types and trait types (visitors)
176        // Exclude trait types from opaque_types since they use JsVisitorRef instead of Object<'static>
177        // Also exclude capsule types — they do not get #[napi] class wrappers.
178        let opaque_types: AHashSet<String> = api
179            .types
180            .iter()
181            .filter(|t| t.is_opaque && !t.is_trait && !capsule_types.contains_key(&t.name))
182            .map(|t| t.name.clone())
183            .collect();
184        let mutex_types: AHashSet<String> = api
185            .types
186            .iter()
187            .filter(|t| t.is_opaque && generators::type_needs_mutex(t))
188            .map(|t| t.name.clone())
189            .collect();
190        let has_traits = api.types.iter().any(|t| t.is_trait);
191        if !opaque_types.is_empty() || has_traits {
192            builder.add_import("std::sync::Arc");
193        }
194        if !mutex_types.is_empty() {
195            builder.add_import("std::sync::Mutex");
196        }
197
198        let exclude_types: ahash::AHashSet<String> = config
199            .node
200            .as_ref()
201            .map(|c| c.exclude_types.iter().cloned().collect())
202            .unwrap_or_default();
203
204        // Build adapter body map before type iteration so bodies are available for method generation.
205        let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
206
207        // Map "OwnerType.method" -> streaming item type. The napi backend needs to
208        // override the IR-declared `String` return type with `Vec<{prefix}{item}>`
209        // for streaming adapters, since the generated body returns chunks directly
210        // as a JS array instead of a serialized JSON string.
211        let streaming_item_types: ahash::AHashMap<String, String> = config
212            .adapters
213            .iter()
214            .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
215            .filter_map(|a| {
216                let owner = a.owner_type.as_deref()?;
217                let item = a.item_type.as_deref()?;
218                Some((format!("{owner}.{}", a.name), item.to_string()))
219            })
220            .collect();
221
222        // JsBytes: a newtype wrapper for Vec<u8> with custom FromNapiValue that accepts
223        // Buffer.from(...) from JavaScript. Fixes NAPI v3 macro-derived deserialization
224        // of Vec<u8> fields in #[napi(object)] structs, which normally expect Array[number].
225        let js_bytes_def = r#"
226/// Wrapper for byte arrays that implements custom FromNapiValue to accept Buffer.from(...).
227///
228/// NAPI v3's default FromNapiValue for Vec<u8> expects Array[number], not Buffer.
229/// This wrapper provides custom deserialization that accepts Buffer, Uint8Array, or Array,
230/// converting them to Vec<u8>. Implements Clone and serde traits for use in struct fields.
231#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
232pub struct JsBytes(pub Vec<u8>);
233
234impl From<Vec<u8>> for JsBytes {
235    fn from(v: Vec<u8>) -> Self {
236        JsBytes(v)
237    }
238}
239
240impl From<JsBytes> for Vec<u8> {
241    fn from(js_bytes: JsBytes) -> Self {
242        js_bytes.0
243    }
244}
245
246impl AsRef<[u8]> for JsBytes {
247    fn as_ref(&self) -> &[u8] {
248        &self.0
249    }
250}
251
252impl std::ops::Deref for JsBytes {
253    type Target = Vec<u8>;
254    fn deref(&self) -> &Self::Target {
255        &self.0
256    }
257}
258
259impl std::ops::DerefMut for JsBytes {
260    fn deref_mut(&mut self) -> &mut Self::Target {
261        &mut self.0
262    }
263}
264
265impl napi::bindgen_prelude::FromNapiValue for JsBytes {
266    unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
267        use napi::bindgen_prelude::FromNapiValue;
268
269        // Try Buffer first (most common for binary data in JS)
270        if let Ok(buffer) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, napi_val) } {
271            return Ok(JsBytes(buffer.as_ref().to_vec()));
272        }
273
274        // Try Uint8Array
275        if let Ok(ua) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, napi_val) } {
276            return Ok(JsBytes(ua.to_vec()));
277        }
278
279        // Fall back to Array[number]
280        if let Ok(vec) = unsafe { Vec::<u8>::from_napi_value(env, napi_val) } {
281            return Ok(JsBytes(vec));
282        }
283
284        Err(napi::Error::new(
285            napi::Status::InvalidArg,
286            "Expected Buffer, Uint8Array, or Array<number> for bytes field",
287        ))
288    }
289}
290
291impl napi::bindgen_prelude::ToNapiValue for JsBytes {
292    unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {
293        // Delegate to Vec<u8>'s implementation (which returns an Uint8Array/Buffer).
294        unsafe { <Vec<u8> as napi::bindgen_prelude::ToNapiValue>::to_napi_value(env, val.0) }
295    }
296}
297"#;
298        builder.add_item(js_bytes_def);
299
300        // JsVisitorRef: a thin wrapper around napi::Object that implements Clone.
301        // This newtype makes Object<'static> work with napi(object) field derivations,
302        // which require Clone. Uses std::sync::Arc to make the handle cheaply cloneable.
303        if has_traits {
304            let js_visitor_ref_def = r#"
305/// Wrapper for trait visitor types (napi::Object<'static>) that implements Clone.
306///
307/// Object is not Clone. This wrapper uses Arc<Object<'static>> internally for cheap cloning.
308/// The .inner field is public for compatibility with generated code that needs to access
309/// the underlying Object for trait dispatch.
310pub struct JsVisitorRef {
311    pub inner: std::sync::Arc<napi::bindgen_prelude::Object<'static>>,
312}
313
314impl Clone for JsVisitorRef {
315    fn clone(&self) -> Self {
316        JsVisitorRef {
317            inner: std::sync::Arc::clone(&self.inner),
318        }
319    }
320}
321
322#[allow(clippy::arc_with_non_send_sync)]
323impl From<napi::bindgen_prelude::Object<'static>> for JsVisitorRef {
324    fn from(visitor: napi::bindgen_prelude::Object<'static>) -> Self {
325        JsVisitorRef {
326            inner: std::sync::Arc::new(visitor),
327        }
328    }
329}
330
331impl From<JsVisitorRef> for napi::bindgen_prelude::Object<'static> {
332    fn from(visitor_ref: JsVisitorRef) -> Self {
333        // Object<'static> is Copy (it just holds an env+handle pair), so deref directly.
334        *visitor_ref.inner
335    }
336}
337"#;
338            builder.add_item(js_visitor_ref_def);
339        }
340
341        // Emit adapter-generated standalone items (streaming iterators, callback bridges).
342        for adapter in &config.adapters {
343            match adapter.pattern {
344                alef_core::config::AdapterPattern::Streaming => {
345                    let key = alef_adapters::stream_struct_key(adapter);
346                    if let Some(struct_code) = adapter_bodies.get(&key) {
347                        builder.add_item(struct_code);
348                    }
349                }
350                alef_core::config::AdapterPattern::CallbackBridge => {
351                    let struct_key = format!("{}.__bridge_struct__", adapter.name);
352                    let impl_key = format!("{}.__bridge_impl__", adapter.name);
353                    if let Some(struct_code) = adapter_bodies.get(&struct_key) {
354                        builder.add_item(struct_code);
355                    }
356                    if let Some(impl_code) = adapter_bodies.get(&impl_key) {
357                        builder.add_item(impl_code);
358                    }
359                }
360                _ => {}
361            }
362        }
363
364        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
365        // and custom constructor. Use shared generators for enums and functions,
366        // but keep struct/method generation custom.
367        for typ in api
368            .types
369            .iter()
370            .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
371            .filter(|typ| !typ.name.ends_with("Builder") && !typ.name.ends_with("Update"))
372        {
373            // Capsule types bypass #[napi] class emission entirely — they are exposed
374            // as raw External<T> pointers in JsObject wrappers from functions that return them.
375            if capsule_types.contains_key(&typ.name) {
376                continue;
377            }
378            if typ.is_opaque {
379                // gen_opaque_struct_prefixed emits `#[napi]` from cfg.struct_attrs.
380                // Replace with `#[napi(js_name = "Foo")]` so NAPI-RS exports the
381                // unprefixed name while the Rust struct stays JsFoo internally.
382                // Prepend `///` rustdoc so napi-derive forwards it to JSDoc on
383                // the corresponding `export declare class` in index.d.ts.
384                let opaque_struct_code = {
385                    let raw = alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, &prefix);
386                    let struct_name = format!("{prefix}{}", typ.name);
387                    let body = raw.replace(
388                        &format!("#[napi]pub struct {struct_name}"),
389                        &format!("#[napi(js_name = \"{}\")]pub struct {struct_name}", typ.name),
390                    );
391                    let mut out = String::new();
392                    let sanitized_doc = alef_codegen::doc_emission::sanitize_rust_idioms(
393                        &typ.doc,
394                        alef_codegen::doc_emission::DocTarget::TsDoc,
395                    );
396                    alef_codegen::doc_emission::emit_rustdoc(&mut out, &sanitized_doc, "");
397                    out.push_str(&body);
398                    out
399                };
400                builder.add_item(&opaque_struct_code);
401                let capsule_type_names: AHashSet<String> = capsule_types.keys().cloned().collect();
402                builder.add_item(&types::gen_opaque_struct_methods(
403                    typ,
404                    &mapper,
405                    &cfg,
406                    &opaque_types,
407                    &prefix,
408                    &adapter_bodies,
409                    &streaming_item_types,
410                    &capsule_type_names,
411                    &mutex_types,
412                    &capsule_types,
413                ));
414                // Client constructor — emit a #[napi(constructor)] impl
415                if let Some(ctor) = config.client_constructors.get(&typ.name) {
416                    let struct_name = format!("{prefix}{}", typ.name);
417                    let ctor_body = alef_codegen::generators::gen_opaque_constructor(
418                        ctor,
419                        &typ.name,
420                        &core_import,
421                        "#[napi(constructor)]",
422                    );
423                    let ctor_impl = format!("#[napi]\nimpl {struct_name} {{\n{}}}", ctor_body);
424                    builder.add_item(&ctor_impl);
425                }
426            } else {
427                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
428                // napi(object) structs cannot have #[napi] impl blocks.
429                // gen_struct adds Default to derives when typ.has_default is true.
430                builder.add_item(&types::gen_struct(
431                    typ,
432                    &mapper,
433                    &prefix,
434                    has_serde,
435                    &opaque_types,
436                    &never_skip_cfg_field_names,
437                ));
438                // Emit impl methods as standalone #[napi] free functions.
439                // #[napi(object)] structs cannot have impl blocks, so each method becomes a
440                // module-level function whose JS name encodes the type as a namespace prefix
441                // (e.g. `processConfigAll`, `processConfigWithChunking`). TypeScript callers
442                // group them via a value-namespace declaration in their own code.
443                let dto_fns = types::gen_dto_method_fns(typ, &mapper, &cfg, &opaque_types, &prefix, &mutex_types, api);
444                if !dto_fns.is_empty() {
445                    builder.add_item(&dto_fns);
446                }
447            }
448        }
449
450        // Collect struct names so tagged enum codegen knows which Named types have binding structs
451        let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
452
453        // Collect Named types that have a Default impl. These are eligible to be
454        // promoted to Option<T> in binding signatures so JS callers may pass
455        // `undefined` to fall back to a default-constructed instance.
456        let default_types: ahash::AHashSet<String> = api
457            .types
458            .iter()
459            .filter(|t| t.has_default)
460            .map(|t| t.name.clone())
461            .collect();
462
463        for enum_def in &api.enums {
464            builder.add_item(&enums::gen_enum(enum_def, &prefix, has_serde));
465        }
466
467        let exclude_functions: ahash::AHashSet<String> = config
468            .node
469            .as_ref()
470            .map(|c| c.exclude_functions.iter().cloned().collect())
471            .unwrap_or_default();
472
473        for func in &api.functions {
474            if exclude_functions.contains(&func.name) {
475                continue;
476            }
477            if alef_codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
478                continue;
479            }
480            let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
481            let options_field_bridge = crate::trait_bridge::find_options_field_binding(func, &config.trait_bridges)
482                // Only use the options-field path when the bridge field actually survives
483                // into the binding struct. If the core field is `#[cfg(...)]`-gated, the
484                // struct generator strips it and the generated bridge code would reference
485                // a missing field, producing `E0609 no field` at compile time.
486                // Exception: fields listed in never_skip_cfg_field_names are cfg-gated but
487                // preserved by the struct generator, so they are valid for bridge codegen.
488                .filter(|(_, bridge_cfg)| {
489                    let Some(field_name) = bridge_cfg.resolved_options_field() else { return false; };
490                    let Some(options_type) = bridge_cfg.options_type.as_deref() else { return false; };
491                    api.types
492                        .iter()
493                        .filter(|t| t.name == options_type)
494                        .flat_map(|t| t.fields.iter())
495                        .any(|f| f.name == field_name && (f.cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_name)))
496                });
497            // Skip sanitized functions when there's no trait bridge that can replace the
498            // sanitized parameter — such functions cannot be auto-delegated. Functions
499            // whose only "sanitized" param is a configured trait_bridge param (e.g.
500            // Option<VisitorHandle> in html-to-markdown) are emitted via gen_bridge_function.
501            if func.sanitized && bridge_param.is_none() && options_field_bridge.is_none() {
502                continue;
503            }
504            if let Some((param_idx, bridge_cfg)) = bridge_param {
505                builder.add_item(&crate::trait_bridge::gen_bridge_function(
506                    func,
507                    param_idx,
508                    bridge_cfg,
509                    &mapper,
510                    &cfg,
511                    &Default::default(),
512                    &opaque_types,
513                    &core_import,
514                ));
515            } else if let Some((param_idx, bridge_cfg)) = options_field_bridge {
516                builder.add_item(&crate::trait_bridge::gen_options_field_bridge_function(
517                    func,
518                    param_idx,
519                    bridge_cfg,
520                    &mapper,
521                    &cfg,
522                    &opaque_types,
523                    &core_import,
524                ));
525            } else if !capsule_types.is_empty() && capsule::function_involves_capsule(func, &capsule_types) {
526                // Function returns a capsule type — emit a napi shim that returns JsObject
527                // with __parser = External<T>(ptr from value.into_raw()).
528                // JsObjectValue provides set_named_property; imported once below.
529                builder.add_item(&capsule::gen_capsule_function(func, &capsule_types, &core_import));
530            } else {
531                builder.add_item(&functions::gen_function(
532                    func,
533                    &mapper,
534                    &cfg,
535                    &opaque_types,
536                    &default_types,
537                    &prefix,
538                    &capsule_types,
539                    &mutex_types,
540                ));
541            }
542        }
543
544        // Emit module-level wrapper functions for adapters (streaming methods).
545        for adapter in &config.adapters {
546            builder.add_item(&functions::gen_adapter_wrapper(adapter, &core_import));
547        }
548
549        // Trait bridge wrappers — generate NAPI bridge structs that delegate to JS objects
550        for bridge_cfg in &config.trait_bridges {
551            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
552                let bridge = crate::trait_bridge::gen_trait_bridge(
553                    trait_type,
554                    bridge_cfg,
555                    &core_import,
556                    &config.error_type_name(),
557                    &config.error_constructor_expr(),
558                    api,
559                );
560                for imp in &bridge.imports {
561                    builder.add_import(imp);
562                }
563                builder.add_item(&bridge.code);
564            }
565        }
566
567        let binding_to_core = alef_codegen::conversions::convertible_types(api);
568        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
569        let input_types = alef_codegen::conversions::input_type_names(api);
570        // NOTE: NAPI does NOT populate `trait_bridge_arc_wrapper_field_names`. Unlike
571        // PHP/WASM which wrap their visitor handle as `WrapperType { inner: Arc<...> }`,
572        // the NAPI binding stores the raw JS `napi::bindgen_prelude::Object` directly on
573        // `JsConversionOptions.visitor`. There is no `.inner` field to dereference, so
574        // the `(*v.inner).clone()` substitution would emit code that fails to compile.
575        // Instead, the NAPI `convert` codegen attaches the visitor in a post-process
576        // step after the `From<JsConversionOptions>` impl runs (`o.visitor = None;`
577        // then `result.visitor = visitor_handle.clone()`), so the From impl harmlessly
578        // emits `Default::default()` for the visitor field.
579        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
580            type_name_prefix: &prefix,
581            cast_large_ints_to_i64: true,
582            cast_f32_to_f64: true,
583            // optionalize_defaults: For types with has_default, conversion generators
584            // make all fields Option<T> and apply defaults via FromNapiValue,
585            // enabling JS users to pass partial objects and omit fields they want defaults for.
586            optionalize_defaults: true,
587            option_duration_on_defaults: true,
588            include_cfg_metadata: true,
589            // Pass opaque_types so the conversion generator can emit `Default::default()`
590            // for opaque-type fields (e.g. visitor: Object<'static>) instead of trying to
591            // convert them via Into — these fields are handled separately via bridge code.
592            opaque_types: Some(&opaque_types),
593            // Json fields are stored as serde_json::Value in the binding so JS
594            // callers can pass objects/arrays/scalars directly.
595            json_as_value: true,
596            never_skip_cfg_field_names: &never_skip_cfg_field_names,
597            ..Default::default()
598        };
599        // From/Into conversions using shared parameterized generators.
600        // Exclude Builder/Update DTOs — their struct definitions are filtered out of
601        // emission upstream, so emitting `From<JsXxxUpdate> for core::XxxUpdate` would
602        // reference an undefined `JsXxxUpdate` type.
603        for typ in api
604            .types
605            .iter()
606            .filter(|typ| !typ.is_trait)
607            .filter(|typ| !typ.name.ends_with("Builder") && !typ.name.ends_with("Update"))
608        {
609            if input_types.contains(&typ.name)
610                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
611            {
612                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
613                    typ,
614                    &core_import,
615                    &napi_conv_config,
616                ));
617            }
618            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
619                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
620                    typ,
621                    &core_import,
622                    &opaque_types,
623                    &napi_conv_config,
624                ));
625            }
626        }
627        let mut emitted_enum_binding_to_core: AHashSet<String> = AHashSet::new();
628        for e in &api.enums {
629            let has_data_variants = e.variants.iter().any(|v| !v.fields.is_empty());
630            let is_tagged_data_enum = e.serde_tag.is_some() && has_data_variants;
631            let is_untagged_data_enum = e.serde_untagged && has_data_variants;
632            if is_tagged_data_enum {
633                // Tagged data enums use flattened struct — generate custom conversions
634                builder.add_item(&methods::gen_tagged_enum_binding_to_core(
635                    e,
636                    &core_import,
637                    &prefix,
638                    &struct_names,
639                ));
640                builder.add_item(&methods::gen_tagged_enum_core_to_binding(
641                    e,
642                    &core_import,
643                    &prefix,
644                    &struct_names,
645                ));
646            } else if is_untagged_data_enum {
647                // Untagged data enums are wrapped around serde_json::Value — bridge via serde.
648                let binding_name = format!("{prefix}{}", e.name);
649                let core_path = alef_codegen::conversions::core_enum_path_remapped(
650                    e,
651                    &core_import,
652                    napi_conv_config.source_crate_remaps,
653                );
654                builder.add_item(&format!(
655                    "impl From<{binding_name}> for {core_path} {{\n    \
656                         fn from(val: {binding_name}) -> Self {{\n        \
657                             serde_json::from_value(val.0).unwrap_or_default()\n    \
658                         }}\n\
659                     }}\n"
660                ));
661                builder.add_item(&format!(
662                    "impl From<{core_path}> for {binding_name} {{\n    \
663                         fn from(val: {core_path}) -> Self {{\n        \
664                             Self(serde_json::to_value(val).unwrap_or_default())\n    \
665                         }}\n\
666                     }}\n"
667                ));
668            } else {
669                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
670                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
671                        e,
672                        &core_import,
673                        &napi_conv_config,
674                    ));
675                    emitted_enum_binding_to_core.insert(e.name.clone());
676                }
677                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
678                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
679                        e,
680                        &core_import,
681                        &napi_conv_config,
682                    ));
683                }
684            }
685        }
686
687        // From impls for tagged data enums lowered to flat NAPI classes.
688        // Track types whose `From<binding> for core` impl has already been emitted by
689        // the main loop above (or by a prior variant in this loop) to avoid duplicate
690        // impls when the same DTO appears both as a top-level input type and as a
691        // variant payload of a tagged enum (e.g. `CrawlPageResult` used directly and
692        // inside `CrawlEvent::Page { result: Box<CrawlPageResult> }`).
693        // The main loop above emits a `From<binding> for core` impl for any type
694        // that is `input_types.contains(&typ.name)`. Pre-seed the dedup set with those.
695        let mut emitted_binding_to_core: AHashSet<String> = api
696            .types
697            .iter()
698            .filter(|typ| !typ.is_trait && input_types.contains(&typ.name))
699            .filter(|typ| !typ.name.ends_with("Builder") && !typ.name.ends_with("Update"))
700            .filter(|typ| alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core))
701            .map(|typ| typ.name.clone())
702            .collect();
703        for enum_def in api.enums.iter() {
704            let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
705            let is_tagged_data_enum = enum_def.serde_tag.is_some() && has_data_variants;
706            if !is_tagged_data_enum {
707                continue;
708            }
709            // Also generate From impls for variant data types (e.g., BibtexMetadata from Metadata::Bibtex).
710            // These are needed when tagged enum binding→core conversion calls `.into()` on variant fields.
711            for variant in &enum_def.variants {
712                for field in &variant.fields {
713                    if let TypeRef::Named(type_name) = &field.ty {
714                        if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
715                            if emitted_binding_to_core.contains(&typ.name) {
716                                continue;
717                            }
718                            if alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core) {
719                                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
720                                    typ,
721                                    &core_import,
722                                    &napi_conv_config,
723                                ));
724                                emitted_binding_to_core.insert(typ.name.clone());
725                            }
726                        }
727                    }
728                }
729            }
730        }
731
732        // Emit From impls for all remaining DTO types that are convertible but haven't been
733        // emitted yet. This handles nested types that appear as fields in output structures
734        // but are not direct input types or enum variant payloads (e.g., DbfFieldInfo inside
735        // Vec<DbfFieldInfo> in DbfMetadata, which itself is inside FormatMetadata::Dbf).
736        for typ in api
737            .types
738            .iter()
739            .filter(|t| !t.is_trait)
740            .filter(|t| !t.name.ends_with("Builder") && !t.name.ends_with("Update"))
741        {
742            if !emitted_binding_to_core.contains(&typ.name)
743                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
744            {
745                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
746                    typ,
747                    &core_import,
748                    &napi_conv_config,
749                ));
750                emitted_binding_to_core.insert(typ.name.clone());
751            }
752        }
753
754        // Emit From impls for unit-variant enums that appear in fields of types we've
755        // already emitted From impls for. These are needed because binding code calls
756        // `.map(Into::into)` on Option<enum> fields (e.g., Option<TextDirection>).
757        for typ in api
758            .types
759            .iter()
760            .filter(|t| !t.is_trait && emitted_binding_to_core.contains(&t.name))
761        {
762            for field in &typ.fields {
763                // Recursively extract enum names from field types (e.g., Option<TextDirection> → TextDirection)
764                fn collect_enum_names(ty: &TypeRef, enums: &mut AHashSet<String>) {
765                    match ty {
766                        TypeRef::Named(name) => {
767                            enums.insert(name.clone());
768                        }
769                        TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_enum_names(inner, enums),
770                        TypeRef::Map(_k, v) => collect_enum_names(v, enums),
771                        _ => {}
772                    }
773                }
774                let mut field_enums = AHashSet::new();
775                collect_enum_names(&field.ty, &mut field_enums);
776                for enum_name in field_enums {
777                    if let Some(enum_def) = api.enums.iter().find(|e| e.name == enum_name) {
778                        // Skip data enums — they have their own conversion logic via tagged enum handling
779                        let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
780                        if enum_def.serde_tag.is_some() && has_data_variants {
781                            continue;
782                        }
783                        if enum_def.serde_untagged && has_data_variants {
784                            continue;
785                        }
786                        // Unit-variant enum: emit From impl for binding→core if not already emitted
787                        if !emitted_enum_binding_to_core.contains(&enum_def.name)
788                            && alef_codegen::conversions::can_generate_enum_conversion(enum_def)
789                        {
790                            builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
791                                enum_def,
792                                &core_import,
793                                &napi_conv_config,
794                            ));
795                            emitted_enum_binding_to_core.insert(enum_def.name.clone());
796                        }
797                    }
798                }
799            }
800        }
801
802        // Error types (variant name constants + converter functions)
803        for error in &api.errors {
804            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
805            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
806            // Emit #[napi] class for errors with introspection methods.
807            let class_code = alef_codegen::error_gen::gen_napi_error_class(error, &core_import);
808            if !class_code.is_empty() {
809                builder.add_item(&class_code);
810            }
811        }
812
813        let mut content = builder.build();
814
815        // Post-process: Fix From<JsXxx> (binding to core) impls to forward visitor field.
816        // The conversion generator emits `__result.visitor = Default::default();` in binding→core
817        // conversions because the raw JS napi::bindgen_prelude::Object is not Clone-able.
818        // This post-process detects that pattern in the JS→Rust direction and replaces it with
819        // code that wraps val.visitor into a JsHtmlVisitorBridge and then into the core Rc<RefCell<>> type.
820        //
821        // Key: only fix `impl From<Js{type}>` (binding→core), NOT `impl From<core_type>` (core→binding).
822        // The core→binding direction correctly uses Default because the Rc<RefCell<>> is opaque to JS.
823        for bridge in &config.trait_bridges {
824            if bridge.bind_via != alef_core::config::BridgeBinding::OptionsField {
825                continue;
826            }
827            if let Some(field_name) = bridge.resolved_options_field() {
828                // Verify the field is present in the binding struct (not cfg-gated away)
829                let Some(options_type) = bridge.options_type.as_deref() else {
830                    continue;
831                };
832                let field_in_binding = api
833                    .types
834                    .iter()
835                    .filter(|t| t.name == options_type)
836                    .flat_map(|t| t.fields.iter())
837                    .any(|f| f.cfg.is_none() && f.name == field_name);
838                if !field_in_binding {
839                    continue;
840                }
841
842                // Find the binding→core conversion impl: `impl From<Js{options_type}> for core...`
843                let prefix = config.node_type_prefix();
844                let js_type_name = format!("{prefix}{options_type}");
845                let impl_marker = format!("impl From<{js_type_name}> for {core_import}");
846
847                // Search forward from the impl marker to find its closing brace and visitor wipe.
848                // We only fix the impl that converts FROM the JS binding type.
849                if let Some(impl_start) = content.find(&impl_marker) {
850                    // Find the matching closing brace for this impl block
851                    let from_impl_start = impl_start;
852                    let impl_body = &content[from_impl_start..];
853
854                    // Find the next `}` that closes this impl — being careful to count braces
855                    let mut brace_depth = 0;
856                    let mut impl_end = 0;
857                    let mut found_fn_from = false;
858                    for (i, ch) in impl_body.char_indices() {
859                        if ch == '{' {
860                            brace_depth += 1;
861                            // Once we see the opening brace of `fn from(...) {`, mark it
862                            if impl_body[..i].contains("fn from") {
863                                found_fn_from = true;
864                            }
865                        } else if ch == '}' {
866                            brace_depth -= 1;
867                            if brace_depth == 0 && found_fn_from {
868                                impl_end = i;
869                                break;
870                            }
871                        }
872                    }
873
874                    if impl_end > 0 {
875                        let impl_block = &impl_body[..impl_end];
876                        let pattern = "__result.visitor = Default::default();";
877
878                        if let Some(rel_pos) = impl_block.find(pattern) {
879                            let pos = from_impl_start + rel_pos;
880                            let before = &content[..pos];
881                            let after = &content[pos + pattern.len()..];
882
883                            // Build the replacement that wraps val.visitor into JsHtmlVisitorBridge
884                            // and then into the core Arc<Mutex<...>> type.
885                            let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
886                            let handle_path = format!("{core_import}::visitor::{type_alias}");
887                            let replacement = format!(
888                                "__result.visitor = val.{field_name}.map(|obj| {{\n            \
889                                    let bridge = JsHtmlVisitorBridge::new(obj);\n            \
890                                    std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n        \
891                                }});"
892                            );
893
894                            content = format!("{}{}{}", before, replacement, after);
895                        }
896                    }
897                }
898            }
899        }
900
901        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
902
903        Ok(vec![GeneratedFile {
904            path: PathBuf::from(&output_dir).join("lib.rs"),
905            content,
906            generated_header: false,
907        }])
908    }
909
910    fn generate_type_stubs(
911        &self,
912        api: &ApiSurface,
913        config: &ResolvedCrateConfig,
914    ) -> anyhow::Result<Vec<GeneratedFile>> {
915        let prefix = config.node_type_prefix();
916        let exclude_functions: ahash::AHashSet<String> = config
917            .node
918            .as_ref()
919            .map(|c| c.exclude_functions.iter().cloned().collect())
920            .unwrap_or_default();
921        let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
922            .node
923            .as_ref()
924            .map(|c| c.capsule_types.clone())
925            .unwrap_or_default();
926        let streaming_item_types: ahash::AHashMap<String, String> = config
927            .adapters
928            .iter()
929            .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
930            .filter_map(|a| {
931                let owner = a.owner_type.as_deref()?;
932                let item = a.item_type.as_deref()?;
933                Some((format!("{owner}.{}", a.name), item.to_string()))
934            })
935            .collect();
936        let content = errors::gen_dts(
937            api,
938            &prefix,
939            &exclude_functions,
940            &config.trait_bridges,
941            &capsule_types,
942            &streaming_item_types,
943        );
944
945        // `output_for("node")` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
946        // `index.d.ts` belongs at the crate root, one level up from `src/`.
947        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
948        // Falls back to `crates/{name}-node/` if no node output is configured.
949        let src_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
950        let crate_root = {
951            let p = PathBuf::from(&src_dir);
952            match p.file_name().and_then(|n| n.to_str()) {
953                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
954                _ => p,
955            }
956        };
957
958        Ok(vec![GeneratedFile {
959            path: crate_root.join("index.d.ts"),
960            content,
961            generated_header: false,
962        }])
963    }
964
965    fn build_config(&self) -> Option<BuildConfig> {
966        Some(BuildConfig {
967            tool: "napi",
968            crate_suffix: "-node",
969            build_dep: BuildDependency::None,
970            post_build: vec![PostBuildStep::PatchFile {
971                path: "index.d.ts",
972                find: "export declare const enum",
973                replace: "export declare enum",
974            }],
975        })
976    }
977}
978
979/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
980#[cfg(test)]
981mod tests {
982    use super::NapiBackend;
983    use alef_core::backend::Backend;
984    use alef_core::config::Language;
985
986    /// NapiBackend::name returns "napi".
987    #[test]
988    fn napi_backend_name_is_napi() {
989        let b = NapiBackend;
990        assert_eq!(b.name(), "napi");
991    }
992
993    /// NapiBackend::language returns Language::Node.
994    #[test]
995    fn napi_backend_language_is_node() {
996        let b = NapiBackend;
997        assert_eq!(b.language(), Language::Node);
998    }
999
1000    /// Test that cfg-gated fields in never_skip_cfg_field_names pass the options-field-bridge filter.
1001    #[test]
1002    fn cfg_gated_field_accepted_when_in_never_skip_list() {
1003        // Test the predicate logic: a cfg-gated field "visitor" should be accepted
1004        // when it appears in never_skip_cfg_field_names.
1005        let never_skip_cfg_field_names = ["visitor".to_string()];
1006        let field_is_target = "visitor";
1007
1008        // Simulate a field with cfg = Some(...)
1009        let field_has_cfg = Some("feature = \"visitor\"");
1010
1011        // Predicate: f.cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_name)
1012        let accepted = field_has_cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_is_target);
1013
1014        assert!(
1015            accepted,
1016            "cfg-gated field 'visitor' should pass filter when in never_skip_cfg_field_names"
1017        );
1018    }
1019}