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_codegen::naming::to_node_name;
15use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
16use alef_core::config::{Language, NodeCapsuleTypeConfig, ResolvedCrateConfig, resolve_output_dir};
17use alef_core::ir::{ApiSurface, TypeRef};
18use std::collections::HashMap;
19use std::path::PathBuf;
20
21pub struct NapiBackend;
22
23impl NapiBackend {
24 fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
25 RustBindingConfig {
26 struct_attrs: &["napi"],
27 field_attrs: &[],
28 struct_derives: &["Clone"],
29 method_block_attr: Some("napi"),
30 constructor_attr: "#[napi(constructor)]",
31 static_attr: None,
32 function_attr: "#[napi]",
33 enum_attrs: &["napi(string_enum)"],
34 enum_derives: &["Clone"],
35 needs_signature: false,
36 signature_prefix: "",
37 signature_suffix: "",
38 core_import,
39 async_pattern: AsyncPattern::NapiNativeAsync,
40 has_serde,
41 type_name_prefix: prefix,
43 option_duration_on_defaults: true,
44 opaque_type_names: &[],
45 skip_impl_constructor: false,
46 cast_uints_to_i32: false,
47 cast_large_ints_to_f64: false,
48 named_non_opaque_params_by_ref: false,
49 lossy_skip_types: &[],
50 serializable_opaque_type_names: &[],
51 never_skip_cfg_field_names: &[],
52 }
53 }
54}
55
56impl Backend for NapiBackend {
57 fn name(&self) -> &str {
58 "napi"
59 }
60
61 fn language(&self) -> Language {
62 Language::Node
63 }
64
65 fn capabilities(&self) -> Capabilities {
66 Capabilities {
67 supports_async: true,
68 supports_classes: true,
69 supports_enums: true,
70 supports_option: true,
71 supports_result: true,
72 ..Capabilities::default()
73 }
74 }
75
76 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
77 let prefix = config.node_type_prefix();
78 let trait_type_names: AHashSet<String> = api
79 .types
80 .iter()
81 .filter(|t| t.is_trait)
82 .map(|t| t.name.clone())
83 .collect();
84 let capsule_type_names_for_mapper: AHashSet<String> = config
85 .node
86 .as_ref()
87 .map(|c| c.capsule_types.keys().cloned().collect())
88 .unwrap_or_default();
89 let mapper =
90 NapiMapper::with_traits_and_capsules(prefix.clone(), trait_type_names, capsule_type_names_for_mapper);
91 let core_import = config.core_import_name();
92
93 let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
95 let has_serde = alef_core::config::detect_serde_available(&output_dir);
96 let mut cfg = Self::binding_config(&core_import, &prefix, has_serde);
97 let never_skip_cfg_field_names: Vec<String> = config
98 .trait_bridges
99 .iter()
100 .filter_map(|b| {
101 if b.bind_via == alef_core::config::BridgeBinding::OptionsField {
102 b.resolved_options_field().map(|s| s.to_string())
103 } else {
104 None
105 }
106 })
107 .collect();
108 cfg.never_skip_cfg_field_names = &never_skip_cfg_field_names;
109
110 let mut builder = RustFileBuilder::new().with_generated_header();
111 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
112 builder.add_inner_attribute("allow(unsafe_code)");
113 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)");
114 builder.add_inner_attribute(
119 "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)",
120 );
121 builder.add_import("napi::*");
122 builder.add_import("napi_derive::napi");
123
124 builder.add_import("serde_json");
128
129 for trait_path in generators::collect_trait_imports(api) {
131 builder.add_import(&trait_path);
132 }
133
134 let has_maps = api
136 .types
137 .iter()
138 .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
139 || api
140 .functions
141 .iter()
142 .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
143 if has_maps {
144 builder.add_import("std::collections::HashMap");
145 }
146
147 let has_async =
152 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
153
154 if has_async {
155 builder.add_item(&functions::gen_tokio_runtime());
156 }
157
158 let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
161 .node
162 .as_ref()
163 .map(|c| c.capsule_types.clone())
164 .unwrap_or_default();
165
166 if !capsule_types.is_empty() {
169 builder.add_import("napi::bindgen_prelude::JsObjectValue");
170 builder.add_item(&capsule::gen_ffi_declarations());
173 let constants = capsule::gen_type_tag_constants(&capsule_types);
174 if !constants.is_empty() {
175 builder.add_item(&constants);
176 }
177 }
178
179 let opaque_types: AHashSet<String> = api
183 .types
184 .iter()
185 .filter(|t| t.is_opaque && !t.is_trait && !capsule_types.contains_key(&t.name))
186 .map(|t| t.name.clone())
187 .collect();
188 let mutex_types: AHashSet<String> = api
189 .types
190 .iter()
191 .filter(|t| t.is_opaque && generators::type_needs_mutex(t))
192 .map(|t| t.name.clone())
193 .collect();
194 let has_traits = api.types.iter().any(|t| t.is_trait);
195 if !opaque_types.is_empty() || has_traits {
196 builder.add_import("std::sync::Arc");
197 }
198 if !mutex_types.is_empty() {
199 builder.add_import("std::sync::Mutex");
200 }
201
202 let exclude_types: ahash::AHashSet<String> = config
203 .node
204 .as_ref()
205 .map(|c| c.exclude_types.iter().cloned().collect())
206 .unwrap_or_default();
207
208 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
210
211 let streaming_item_types: ahash::AHashMap<String, String> = config
216 .adapters
217 .iter()
218 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
219 .filter_map(|a| {
220 let owner = a.owner_type.as_deref()?;
221 let item = a.item_type.as_deref()?;
222 Some((format!("{owner}.{}", a.name), item.to_string()))
223 })
224 .collect();
225
226 let js_bytes_def = r#"
230/// Wrapper for byte arrays that implements custom FromNapiValue to accept Buffer.from(...).
231///
232/// NAPI v3's default FromNapiValue for Vec<u8> expects Array[number], not Buffer.
233/// This wrapper provides custom deserialization that accepts Buffer, Uint8Array, or Array,
234/// converting them to Vec<u8>. Implements Clone and serde traits for use in struct fields.
235#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
236pub struct JsBytes(pub Vec<u8>);
237
238impl From<Vec<u8>> for JsBytes {
239 fn from(v: Vec<u8>) -> Self {
240 JsBytes(v)
241 }
242}
243
244impl From<JsBytes> for Vec<u8> {
245 fn from(js_bytes: JsBytes) -> Self {
246 js_bytes.0
247 }
248}
249
250impl AsRef<[u8]> for JsBytes {
251 fn as_ref(&self) -> &[u8] {
252 &self.0
253 }
254}
255
256impl std::ops::Deref for JsBytes {
257 type Target = Vec<u8>;
258 fn deref(&self) -> &Self::Target {
259 &self.0
260 }
261}
262
263impl std::ops::DerefMut for JsBytes {
264 fn deref_mut(&mut self) -> &mut Self::Target {
265 &mut self.0
266 }
267}
268
269impl napi::bindgen_prelude::FromNapiValue for JsBytes {
270 unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
271 use napi::bindgen_prelude::FromNapiValue;
272
273 // Try Buffer first (most common for binary data in JS)
274 if let Ok(buffer) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, napi_val) } {
275 return Ok(JsBytes(buffer.as_ref().to_vec()));
276 }
277
278 // Try Uint8Array
279 if let Ok(ua) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, napi_val) } {
280 return Ok(JsBytes(ua.to_vec()));
281 }
282
283 // Fall back to Array[number]
284 if let Ok(vec) = unsafe { Vec::<u8>::from_napi_value(env, napi_val) } {
285 return Ok(JsBytes(vec));
286 }
287
288 Err(napi::Error::new(
289 napi::Status::InvalidArg,
290 "Expected Buffer, Uint8Array, or Array<number> for bytes field",
291 ))
292 }
293}
294
295impl napi::bindgen_prelude::ToNapiValue for JsBytes {
296 unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {
297 // Delegate to Vec<u8>'s implementation (which returns an Uint8Array/Buffer).
298 unsafe { <Vec<u8> as napi::bindgen_prelude::ToNapiValue>::to_napi_value(env, val.0) }
299 }
300}
301"#;
302 builder.add_item(js_bytes_def);
303
304 if has_traits {
308 let js_visitor_ref_def = r#"
309/// Wrapper for trait visitor types (napi::Object<'static>) that implements Clone.
310///
311/// Object is not Clone. This wrapper uses Arc<Object<'static>> internally for cheap cloning.
312/// The .inner field is public for compatibility with generated code that needs to access
313/// the underlying Object for trait dispatch.
314pub struct JsVisitorRef {
315 pub inner: std::sync::Arc<napi::bindgen_prelude::Object<'static>>,
316}
317
318impl Clone for JsVisitorRef {
319 fn clone(&self) -> Self {
320 JsVisitorRef {
321 inner: std::sync::Arc::clone(&self.inner),
322 }
323 }
324}
325
326#[allow(clippy::arc_with_non_send_sync)]
327impl From<napi::bindgen_prelude::Object<'static>> for JsVisitorRef {
328 fn from(visitor: napi::bindgen_prelude::Object<'static>) -> Self {
329 JsVisitorRef {
330 inner: std::sync::Arc::new(visitor),
331 }
332 }
333}
334
335impl From<JsVisitorRef> for napi::bindgen_prelude::Object<'static> {
336 fn from(visitor_ref: JsVisitorRef) -> Self {
337 // Object<'static> is Copy (it just holds an env+handle pair), so deref directly.
338 *visitor_ref.inner
339 }
340}
341"#;
342 builder.add_item(js_visitor_ref_def);
343 }
344
345 for adapter in &config.adapters {
347 match adapter.pattern {
348 alef_core::config::AdapterPattern::Streaming => {
349 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
350 if let Some(struct_code) = adapter_bodies.get(&key) {
351 builder.add_item(struct_code);
352 }
353 }
354 alef_core::config::AdapterPattern::CallbackBridge => {
355 let struct_key = format!("{}.__bridge_struct__", adapter.name);
356 let impl_key = format!("{}.__bridge_impl__", adapter.name);
357 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
358 builder.add_item(struct_code);
359 }
360 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
361 builder.add_item(impl_code);
362 }
363 }
364 _ => {}
365 }
366 }
367
368 for typ in api
372 .types
373 .iter()
374 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
375 {
376 if capsule_types.contains_key(&typ.name) {
379 continue;
380 }
381 if typ.is_opaque {
382 let opaque_struct_code = {
388 let raw = alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, &prefix);
389 let struct_name = format!("{prefix}{}", typ.name);
390 let body = raw.replace(
391 &format!("#[napi]pub struct {struct_name}"),
392 &format!("#[napi(js_name = \"{}\")]pub struct {struct_name}", typ.name),
393 );
394 let mut out = String::new();
395 alef_codegen::doc_emission::emit_rustdoc(&mut out, &typ.doc, "");
396 out.push_str(&body);
397 out
398 };
399 builder.add_item(&opaque_struct_code);
400 let capsule_type_names: AHashSet<String> = capsule_types.keys().cloned().collect();
401 builder.add_item(&types::gen_opaque_struct_methods(
402 typ,
403 &mapper,
404 &cfg,
405 &opaque_types,
406 &prefix,
407 &adapter_bodies,
408 &streaming_item_types,
409 &capsule_type_names,
410 &mutex_types,
411 &capsule_types,
412 ));
413 } else {
414 builder.add_item(&types::gen_struct(
418 typ,
419 &mapper,
420 &prefix,
421 has_serde,
422 &opaque_types,
423 &never_skip_cfg_field_names,
424 ));
425 }
426 }
427
428 let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
430
431 let default_types: ahash::AHashSet<String> = api
435 .types
436 .iter()
437 .filter(|t| t.has_default)
438 .map(|t| t.name.clone())
439 .collect();
440
441 for enum_def in &api.enums {
442 builder.add_item(&enums::gen_enum(enum_def, &prefix, has_serde));
443 }
444
445 let exclude_functions: ahash::AHashSet<String> = config
446 .node
447 .as_ref()
448 .map(|c| c.exclude_functions.iter().cloned().collect())
449 .unwrap_or_default();
450
451 for func in &api.functions {
452 if exclude_functions.contains(&func.name) {
453 continue;
454 }
455 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
456 let options_field_bridge = crate::trait_bridge::find_options_field_binding(func, &config.trait_bridges)
457 .filter(|(_, bridge_cfg)| {
464 let Some(field_name) = bridge_cfg.resolved_options_field() else { return false; };
465 let Some(options_type) = bridge_cfg.options_type.as_deref() else { return false; };
466 api.types
467 .iter()
468 .filter(|t| t.name == options_type)
469 .flat_map(|t| t.fields.iter())
470 .any(|f| f.name == field_name && (f.cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_name)))
471 });
472 if func.sanitized && bridge_param.is_none() && options_field_bridge.is_none() {
477 continue;
478 }
479 if let Some((param_idx, bridge_cfg)) = bridge_param {
480 builder.add_item(&crate::trait_bridge::gen_bridge_function(
481 func,
482 param_idx,
483 bridge_cfg,
484 &mapper,
485 &cfg,
486 &Default::default(),
487 &opaque_types,
488 &core_import,
489 ));
490 } else if let Some((param_idx, bridge_cfg)) = options_field_bridge {
491 builder.add_item(&crate::trait_bridge::gen_options_field_bridge_function(
492 func,
493 param_idx,
494 bridge_cfg,
495 &mapper,
496 &cfg,
497 &opaque_types,
498 &core_import,
499 ));
500 } else if !capsule_types.is_empty() && capsule::function_involves_capsule(func, &capsule_types) {
501 builder.add_item(&capsule::gen_capsule_function(func, &capsule_types, &core_import));
505 } else {
506 builder.add_item(&functions::gen_function(
507 func,
508 &mapper,
509 &cfg,
510 &opaque_types,
511 &default_types,
512 &prefix,
513 &capsule_types,
514 &mutex_types,
515 ));
516 }
517 }
518
519 for bridge_cfg in &config.trait_bridges {
521 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
522 let bridge = crate::trait_bridge::gen_trait_bridge(
523 trait_type,
524 bridge_cfg,
525 &core_import,
526 &config.error_type_name(),
527 &config.error_constructor_expr(),
528 api,
529 );
530 for imp in &bridge.imports {
531 builder.add_import(imp);
532 }
533 builder.add_item(&bridge.code);
534 }
535 }
536
537 let binding_to_core = alef_codegen::conversions::convertible_types(api);
538 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
539 let input_types = alef_codegen::conversions::input_type_names(api);
540 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
550 type_name_prefix: &prefix,
551 cast_large_ints_to_i64: true,
552 cast_f32_to_f64: true,
553 optionalize_defaults: true,
557 option_duration_on_defaults: true,
558 include_cfg_metadata: true,
559 opaque_types: Some(&opaque_types),
563 json_as_value: true,
566 never_skip_cfg_field_names: &never_skip_cfg_field_names,
567 ..Default::default()
568 };
569 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
571 if input_types.contains(&typ.name)
572 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
573 {
574 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
575 typ,
576 &core_import,
577 &napi_conv_config,
578 ));
579 }
580 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
581 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
582 typ,
583 &core_import,
584 &opaque_types,
585 &napi_conv_config,
586 ));
587 }
588 }
589 for e in &api.enums {
590 let has_data_variants = e.variants.iter().any(|v| !v.fields.is_empty());
591 let is_tagged_data_enum = e.serde_tag.is_some() && has_data_variants;
592 let is_untagged_data_enum = e.serde_untagged && has_data_variants;
593 if is_tagged_data_enum {
594 builder.add_item(&methods::gen_tagged_enum_binding_to_core(
596 e,
597 &core_import,
598 &prefix,
599 &struct_names,
600 ));
601 builder.add_item(&methods::gen_tagged_enum_core_to_binding(
602 e,
603 &core_import,
604 &prefix,
605 &struct_names,
606 ));
607 } else if is_untagged_data_enum {
608 let binding_name = format!("{prefix}{}", e.name);
610 let core_path = alef_codegen::conversions::core_enum_path_remapped(
611 e,
612 &core_import,
613 napi_conv_config.source_crate_remaps,
614 );
615 builder.add_item(&format!(
616 "impl From<{binding_name}> for {core_path} {{\n \
617 fn from(val: {binding_name}) -> Self {{\n \
618 serde_json::from_value(val.0).unwrap_or_default()\n \
619 }}\n\
620 }}\n"
621 ));
622 builder.add_item(&format!(
623 "impl From<{core_path}> for {binding_name} {{\n \
624 fn from(val: {core_path}) -> Self {{\n \
625 Self(serde_json::to_value(val).unwrap_or_default())\n \
626 }}\n\
627 }}\n"
628 ));
629 } else {
630 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
631 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
632 e,
633 &core_import,
634 &napi_conv_config,
635 ));
636 }
637 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
638 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
639 e,
640 &core_import,
641 &napi_conv_config,
642 ));
643 }
644 }
645 }
646
647 for error in &api.errors {
649 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
650 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
651 }
652
653 let mut content = builder.build();
654
655 for bridge in &config.trait_bridges {
664 if bridge.bind_via != alef_core::config::BridgeBinding::OptionsField {
665 continue;
666 }
667 if let Some(field_name) = bridge.resolved_options_field() {
668 let Some(options_type) = bridge.options_type.as_deref() else {
670 continue;
671 };
672 let field_in_binding = api
673 .types
674 .iter()
675 .filter(|t| t.name == options_type)
676 .flat_map(|t| t.fields.iter())
677 .any(|f| f.cfg.is_none() && f.name == field_name);
678 if !field_in_binding {
679 continue;
680 }
681
682 let prefix = config.node_type_prefix();
684 let js_type_name = format!("{prefix}{options_type}");
685 let impl_marker = format!("impl From<{js_type_name}> for {core_import}");
686
687 if let Some(impl_start) = content.find(&impl_marker) {
690 let from_impl_start = impl_start;
692 let impl_body = &content[from_impl_start..];
693
694 let mut brace_depth = 0;
696 let mut impl_end = 0;
697 let mut found_fn_from = false;
698 for (i, ch) in impl_body.char_indices() {
699 if ch == '{' {
700 brace_depth += 1;
701 if impl_body[..i].contains("fn from") {
703 found_fn_from = true;
704 }
705 } else if ch == '}' {
706 brace_depth -= 1;
707 if brace_depth == 0 && found_fn_from {
708 impl_end = i;
709 break;
710 }
711 }
712 }
713
714 if impl_end > 0 {
715 let impl_block = &impl_body[..impl_end];
716 let pattern = "__result.visitor = Default::default();";
717
718 if let Some(rel_pos) = impl_block.find(pattern) {
719 let pos = from_impl_start + rel_pos;
720 let before = &content[..pos];
721 let after = &content[pos + pattern.len()..];
722
723 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
726 let handle_path = format!("{core_import}::visitor::{type_alias}");
727 let replacement = format!(
728 "__result.visitor = val.{field_name}.map(|obj| {{\n \
729 let bridge = JsHtmlVisitorBridge::new(obj);\n \
730 std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n \
731 }});"
732 );
733
734 content = format!("{}{}{}", before, replacement, after);
735 }
736 }
737 }
738 }
739 }
740
741 let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
742
743 Ok(vec![GeneratedFile {
744 path: PathBuf::from(&output_dir).join("lib.rs"),
745 content,
746 generated_header: false,
747 }])
748 }
749
750 fn generate_public_api(
751 &self,
752 api: &ApiSurface,
753 config: &ResolvedCrateConfig,
754 ) -> anyhow::Result<Vec<GeneratedFile>> {
755 let prefix = config.node_type_prefix();
756 let capsule_types_pub: HashMap<String, NodeCapsuleTypeConfig> = config
757 .node
758 .as_ref()
759 .map(|c| c.capsule_types.clone())
760 .unwrap_or_default();
761
762 let mut type_exports = vec![];
764 let mut function_exports = vec![];
765
766 let _ = &prefix; for typ in api.types.iter() {
777 if typ.is_trait {
778 continue;
779 }
780 if capsule_types_pub.contains_key(&typ.name) {
781 continue;
782 }
783 type_exports.push(typ.name.clone());
784 }
785
786 for enum_def in &api.enums {
790 type_exports.push(enum_def.name.clone());
791 }
792
793 for func in &api.functions {
798 let js_name = to_node_name(&func.name);
800 function_exports.push(js_name);
801 }
802
803 for bridge in &config.trait_bridges {
809 if let Some(name) = bridge.register_fn.as_deref() {
810 function_exports.push(to_node_name(name));
811 }
812 if let Some(name) = bridge.unregister_fn.as_deref() {
813 function_exports.push(to_node_name(name));
814 }
815 if let Some(name) = bridge.clear_fn.as_deref() {
816 function_exports.push(to_node_name(name));
817 }
818 }
819
820 type_exports.sort();
822 function_exports.sort();
823
824 let mut lines = vec![
827 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
828 "".to_string(),
829 ];
830
831 if !function_exports.is_empty() {
834 lines.push("export {".to_string());
835 for name in &function_exports {
836 lines.push(format!(" {name},"));
837 }
838 lines.push(format!("}} from '{}';", config.node_package_name()));
839 lines.push("".to_string());
840 }
841 if !type_exports.is_empty() {
842 lines.push("export type {".to_string());
843 for name in &type_exports {
844 lines.push(format!(" {name},"));
845 }
846 lines.push(format!("}} from '{}';", config.node_package_name()));
847 }
848
849 let custom_mods = config.custom_modules.for_language(Language::Node);
851 for module_name in custom_mods {
852 lines.push(format!("export * from './{module_name}';"));
853 }
854
855 let content = lines.join("\n");
856
857 let output_path = PathBuf::from("packages/typescript/src/index.ts");
859
860 Ok(vec![GeneratedFile {
861 path: output_path,
862 content,
863 generated_header: false,
864 }])
865 }
866
867 fn generate_type_stubs(
868 &self,
869 api: &ApiSurface,
870 config: &ResolvedCrateConfig,
871 ) -> anyhow::Result<Vec<GeneratedFile>> {
872 let prefix = config.node_type_prefix();
873 let exclude_functions: ahash::AHashSet<String> = config
874 .node
875 .as_ref()
876 .map(|c| c.exclude_functions.iter().cloned().collect())
877 .unwrap_or_default();
878 let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
879 .node
880 .as_ref()
881 .map(|c| c.capsule_types.clone())
882 .unwrap_or_default();
883 let content = errors::gen_dts(api, &prefix, &exclude_functions, &config.trait_bridges, &capsule_types);
884
885 let src_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
890 let crate_root = {
891 let p = PathBuf::from(&src_dir);
892 match p.file_name().and_then(|n| n.to_str()) {
893 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
894 _ => p,
895 }
896 };
897
898 Ok(vec![GeneratedFile {
899 path: crate_root.join("index.d.ts"),
900 content,
901 generated_header: false,
902 }])
903 }
904
905 fn build_config(&self) -> Option<BuildConfig> {
906 Some(BuildConfig {
907 tool: "napi",
908 crate_suffix: "-node",
909 build_dep: BuildDependency::None,
910 post_build: vec![PostBuildStep::PatchFile {
911 path: "index.d.ts",
912 find: "export declare const enum",
913 replace: "export declare enum",
914 }],
915 })
916 }
917}
918
919#[cfg(test)]
921mod tests {
922 use super::NapiBackend;
923 use alef_core::backend::Backend;
924 use alef_core::config::Language;
925
926 #[test]
928 fn napi_backend_name_is_napi() {
929 let b = NapiBackend;
930 assert_eq!(b.name(), "napi");
931 }
932
933 #[test]
935 fn napi_backend_language_is_node() {
936 let b = NapiBackend;
937 assert_eq!(b.language(), Language::Node);
938 }
939
940 #[test]
942 fn cfg_gated_field_accepted_when_in_never_skip_list() {
943 let never_skip_cfg_field_names = ["visitor".to_string()];
946 let field_is_target = "visitor";
947
948 let field_has_cfg = Some("feature = \"visitor\"");
950
951 let accepted = field_has_cfg.is_none() || never_skip_cfg_field_names.iter().any(|n| n == field_is_target);
953
954 assert!(
955 accepted,
956 "cfg-gated field 'visitor' should pass filter when in never_skip_cfg_field_names"
957 );
958 }
959}