1pub 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 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 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 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 builder.add_import("serde_json");
127
128 for trait_path in generators::collect_trait_imports(api) {
130 builder.add_import(&trait_path);
131 }
132
133 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 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 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 if !capsule_types.is_empty() {
165 builder.add_import("napi::bindgen_prelude::JsObjectValue");
166 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 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 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
206
207 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 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 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 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 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 if capsule_types.contains_key(&typ.name) {
376 continue;
377 }
378 if typ.is_opaque {
379 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 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 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 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 let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
452
453 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 .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 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 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 for adapter in &config.adapters {
546 builder.add_item(&functions::gen_adapter_wrapper(adapter, &core_import));
547 }
548
549 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 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: true,
587 option_duration_on_defaults: true,
588 include_cfg_metadata: true,
589 opaque_types: Some(&opaque_types),
593 json_as_value: true,
596 never_skip_cfg_field_names: &never_skip_cfg_field_names,
597 ..Default::default()
598 };
599 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(impl_start) = content.find(&impl_marker) {
850 let from_impl_start = impl_start;
852 let impl_body = &content[from_impl_start..];
853
854 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 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 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 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#[cfg(test)]
981mod tests {
982 use super::NapiBackend;
983 use alef_core::backend::Backend;
984 use alef_core::config::Language;
985
986 #[test]
988 fn napi_backend_name_is_napi() {
989 let b = NapiBackend;
990 assert_eq!(b.name(), "napi");
991 }
992
993 #[test]
995 fn napi_backend_language_is_node() {
996 let b = NapiBackend;
997 assert_eq!(b.language(), Language::Node);
998 }
999
1000 #[test]
1002 fn cfg_gated_field_accepted_when_in_never_skip_list() {
1003 let never_skip_cfg_field_names = ["visitor".to_string()];
1006 let field_is_target = "visitor";
1007
1008 let field_has_cfg = Some("feature = \"visitor\"");
1010
1011 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}