1pub mod enums;
4pub mod errors;
5pub mod functions;
6pub mod methods;
7pub mod types;
8
9use crate::type_map::NapiMapper;
10use ahash::AHashSet;
11use alef_codegen::builder::RustFileBuilder;
12use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
13use alef_codegen::naming::to_node_name;
14use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
15use alef_core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
16use alef_core::ir::{ApiSurface, TypeRef};
17use std::path::PathBuf;
18
19pub struct NapiBackend;
20
21impl NapiBackend {
22 fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
23 RustBindingConfig {
24 struct_attrs: &["napi"],
25 field_attrs: &[],
26 struct_derives: &["Clone"],
27 method_block_attr: Some("napi"),
28 constructor_attr: "#[napi(constructor)]",
29 static_attr: None,
30 function_attr: "#[napi]",
31 enum_attrs: &["napi(string_enum)"],
32 enum_derives: &["Clone"],
33 needs_signature: false,
34 signature_prefix: "",
35 signature_suffix: "",
36 core_import,
37 async_pattern: AsyncPattern::NapiNativeAsync,
38 has_serde,
39 type_name_prefix: prefix,
41 option_duration_on_defaults: true,
42 opaque_type_names: &[],
43 skip_impl_constructor: false,
44 cast_uints_to_i32: false,
45 cast_large_ints_to_f64: false,
46 named_non_opaque_params_by_ref: false,
47 lossy_skip_types: &[],
48 serializable_opaque_type_names: &[],
49 }
50 }
51}
52
53impl Backend for NapiBackend {
54 fn name(&self) -> &str {
55 "napi"
56 }
57
58 fn language(&self) -> Language {
59 Language::Node
60 }
61
62 fn capabilities(&self) -> Capabilities {
63 Capabilities {
64 supports_async: true,
65 supports_classes: true,
66 supports_enums: true,
67 supports_option: true,
68 supports_result: true,
69 ..Capabilities::default()
70 }
71 }
72
73 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
74 let prefix = config.node_type_prefix();
75 let trait_type_names: AHashSet<String> = api
76 .types
77 .iter()
78 .filter(|t| t.is_trait)
79 .map(|t| t.name.clone())
80 .collect();
81 let mapper = NapiMapper::with_traits(prefix.clone(), trait_type_names);
82 let core_import = config.core_import_name();
83
84 let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
86 let has_serde = alef_core::config::detect_serde_available(&output_dir);
87 let cfg = Self::binding_config(&core_import, &prefix, has_serde);
88
89 let mut builder = RustFileBuilder::new().with_generated_header();
90 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
91 builder.add_inner_attribute("allow(unsafe_code)");
92 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)");
93 builder.add_inner_attribute(
98 "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)",
99 );
100 builder.add_import("napi::*");
101 builder.add_import("napi_derive::napi");
102
103 builder.add_import("serde_json");
107
108 for trait_path in generators::collect_trait_imports(api) {
110 builder.add_import(&trait_path);
111 }
112
113 let has_maps = api
115 .types
116 .iter()
117 .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
118 || api
119 .functions
120 .iter()
121 .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
122 if has_maps {
123 builder.add_import("std::collections::HashMap");
124 }
125
126 let has_async =
131 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
132
133 if has_async {
134 builder.add_item(&functions::gen_tokio_runtime());
135 }
136
137 let opaque_types: AHashSet<String> = api
140 .types
141 .iter()
142 .filter(|t| t.is_opaque && !t.is_trait)
143 .map(|t| t.name.clone())
144 .collect();
145 let has_traits = api.types.iter().any(|t| t.is_trait);
146 if !opaque_types.is_empty() || has_traits {
147 builder.add_import("std::sync::Arc");
148 }
149
150 let exclude_types: ahash::AHashSet<String> = config
151 .node
152 .as_ref()
153 .map(|c| c.exclude_types.iter().cloned().collect())
154 .unwrap_or_default();
155
156 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
158
159 let streaming_item_types: ahash::AHashMap<String, String> = config
164 .adapters
165 .iter()
166 .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
167 .filter_map(|a| {
168 let owner = a.owner_type.as_deref()?;
169 let item = a.item_type.as_deref()?;
170 Some((format!("{owner}.{}", a.name), item.to_string()))
171 })
172 .collect();
173
174 if has_traits {
178 let js_visitor_ref_def = r#"
179/// Wrapper for trait visitor types (napi::Object<'static>) that implements Clone.
180///
181/// Object is not Clone. This wrapper uses Arc<Object<'static>> internally for cheap cloning.
182/// The .inner field is public for compatibility with generated code that needs to access
183/// the underlying Object for trait dispatch.
184pub struct JsVisitorRef {
185 pub inner: std::sync::Arc<napi::bindgen_prelude::Object<'static>>,
186}
187
188impl Clone for JsVisitorRef {
189 fn clone(&self) -> Self {
190 JsVisitorRef {
191 inner: std::sync::Arc::clone(&self.inner),
192 }
193 }
194}
195
196#[allow(clippy::arc_with_non_send_sync)]
197impl From<napi::bindgen_prelude::Object<'static>> for JsVisitorRef {
198 fn from(visitor: napi::bindgen_prelude::Object<'static>) -> Self {
199 JsVisitorRef {
200 inner: std::sync::Arc::new(visitor),
201 }
202 }
203}
204
205impl From<JsVisitorRef> for napi::bindgen_prelude::Object<'static> {
206 fn from(visitor_ref: JsVisitorRef) -> Self {
207 // Object<'static> is Copy (it just holds an env+handle pair), so deref directly.
208 *visitor_ref.inner
209 }
210}
211"#;
212 builder.add_item(js_visitor_ref_def);
213 }
214
215 for adapter in &config.adapters {
217 match adapter.pattern {
218 alef_core::config::AdapterPattern::Streaming => {
219 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
220 if let Some(struct_code) = adapter_bodies.get(&key) {
221 builder.add_item(struct_code);
222 }
223 }
224 alef_core::config::AdapterPattern::CallbackBridge => {
225 let struct_key = format!("{}.__bridge_struct__", adapter.name);
226 let impl_key = format!("{}.__bridge_impl__", adapter.name);
227 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
228 builder.add_item(struct_code);
229 }
230 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
231 builder.add_item(impl_code);
232 }
233 }
234 _ => {}
235 }
236 }
237
238 for typ in api
242 .types
243 .iter()
244 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
245 {
246 if typ.is_opaque {
247 builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
248 typ, &cfg, &prefix,
249 ));
250 builder.add_item(&types::gen_opaque_struct_methods(
251 typ,
252 &mapper,
253 &cfg,
254 &opaque_types,
255 &prefix,
256 &adapter_bodies,
257 &streaming_item_types,
258 ));
259 } else {
260 builder.add_item(&types::gen_struct(typ, &mapper, &prefix, has_serde, &opaque_types));
264 }
265 }
266
267 let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
269
270 let default_types: ahash::AHashSet<String> = api
274 .types
275 .iter()
276 .filter(|t| t.has_default)
277 .map(|t| t.name.clone())
278 .collect();
279
280 for enum_def in &api.enums {
281 builder.add_item(&enums::gen_enum(enum_def, &prefix, has_serde));
282 }
283
284 let exclude_functions: ahash::AHashSet<String> = config
285 .node
286 .as_ref()
287 .map(|c| c.exclude_functions.iter().cloned().collect())
288 .unwrap_or_default();
289
290 for func in &api.functions {
291 if exclude_functions.contains(&func.name) {
292 continue;
293 }
294 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
295 let options_field_bridge = crate::trait_bridge::find_options_field_binding(func, &config.trait_bridges);
296 if func.sanitized && bridge_param.is_none() && options_field_bridge.is_none() {
301 continue;
302 }
303 if let Some((param_idx, bridge_cfg)) = bridge_param {
304 builder.add_item(&crate::trait_bridge::gen_bridge_function(
305 func,
306 param_idx,
307 bridge_cfg,
308 &mapper,
309 &cfg,
310 &Default::default(),
311 &opaque_types,
312 &core_import,
313 ));
314 } else if let Some((param_idx, bridge_cfg)) = options_field_bridge {
315 builder.add_item(&crate::trait_bridge::gen_options_field_bridge_function(
316 func,
317 param_idx,
318 bridge_cfg,
319 &mapper,
320 &cfg,
321 &opaque_types,
322 &core_import,
323 ));
324 } else {
325 builder.add_item(&functions::gen_function(
326 func,
327 &mapper,
328 &cfg,
329 &opaque_types,
330 &default_types,
331 &prefix,
332 ));
333 }
334 }
335
336 for bridge_cfg in &config.trait_bridges {
338 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
339 let bridge = crate::trait_bridge::gen_trait_bridge(
340 trait_type,
341 bridge_cfg,
342 &core_import,
343 &config.error_type_name(),
344 &config.error_constructor_expr(),
345 api,
346 );
347 for imp in &bridge.imports {
348 builder.add_import(imp);
349 }
350 builder.add_item(&bridge.code);
351 }
352 }
353
354 let binding_to_core = alef_codegen::conversions::convertible_types(api);
355 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
356 let input_types = alef_codegen::conversions::input_type_names(api);
357 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
358 type_name_prefix: &prefix,
359 cast_large_ints_to_i64: true,
360 cast_f32_to_f64: true,
361 optionalize_defaults: true,
365 option_duration_on_defaults: true,
366 include_cfg_metadata: true,
367 opaque_types: Some(&opaque_types),
371 json_as_value: true,
374 ..Default::default()
375 };
376 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
378 if input_types.contains(&typ.name)
379 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
380 {
381 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
382 typ,
383 &core_import,
384 &napi_conv_config,
385 ));
386 }
387 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
388 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
389 typ,
390 &core_import,
391 &opaque_types,
392 &napi_conv_config,
393 ));
394 }
395 }
396 for e in &api.enums {
397 let has_data_variants = e.variants.iter().any(|v| !v.fields.is_empty());
398 let is_tagged_data_enum = e.serde_tag.is_some() && has_data_variants;
399 let is_untagged_data_enum = e.serde_untagged && has_data_variants;
400 if is_tagged_data_enum {
401 builder.add_item(&methods::gen_tagged_enum_binding_to_core(
403 e,
404 &core_import,
405 &prefix,
406 &struct_names,
407 ));
408 builder.add_item(&methods::gen_tagged_enum_core_to_binding(
409 e,
410 &core_import,
411 &prefix,
412 &struct_names,
413 ));
414 } else if is_untagged_data_enum {
415 let binding_name = format!("{prefix}{}", e.name);
417 let core_path = alef_codegen::conversions::core_enum_path_remapped(
418 e,
419 &core_import,
420 napi_conv_config.source_crate_remaps,
421 );
422 builder.add_item(&format!(
423 "impl From<{binding_name}> for {core_path} {{\n \
424 fn from(val: {binding_name}) -> Self {{\n \
425 serde_json::from_value(val.0).unwrap_or_default()\n \
426 }}\n\
427 }}\n"
428 ));
429 builder.add_item(&format!(
430 "impl From<{core_path}> for {binding_name} {{\n \
431 fn from(val: {core_path}) -> Self {{\n \
432 Self(serde_json::to_value(val).unwrap_or_default())\n \
433 }}\n\
434 }}\n"
435 ));
436 } else {
437 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
438 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
439 e,
440 &core_import,
441 &napi_conv_config,
442 ));
443 }
444 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
445 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
446 e,
447 &core_import,
448 &napi_conv_config,
449 ));
450 }
451 }
452 }
453
454 for error in &api.errors {
456 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
457 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
458 }
459
460 let content = builder.build();
461
462 let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
463
464 Ok(vec![GeneratedFile {
465 path: PathBuf::from(&output_dir).join("lib.rs"),
466 content,
467 generated_header: false,
468 }])
469 }
470
471 fn generate_public_api(
472 &self,
473 api: &ApiSurface,
474 config: &ResolvedCrateConfig,
475 ) -> anyhow::Result<Vec<GeneratedFile>> {
476 let prefix = config.node_type_prefix();
477
478 let mut type_exports = vec![];
480 let mut function_exports = vec![];
481
482 for typ in api.types.iter() {
488 if typ.is_trait {
489 continue;
490 }
491 type_exports.push(format!("{prefix}{}", typ.name));
492 }
493
494 for enum_def in &api.enums {
498 type_exports.push(format!("{prefix}{}", enum_def.name));
499 }
500
501 for func in &api.functions {
506 let js_name = to_node_name(&func.name);
508 function_exports.push(js_name);
509 }
510
511 type_exports.sort();
513 function_exports.sort();
514
515 let mut lines = vec![
518 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
519 "".to_string(),
520 ];
521
522 if !function_exports.is_empty() {
525 lines.push("export {".to_string());
526 for name in &function_exports {
527 lines.push(format!(" {name},"));
528 }
529 lines.push(format!("}} from '{}';", config.node_package_name()));
530 lines.push("".to_string());
531 }
532 if !type_exports.is_empty() {
533 lines.push("export type {".to_string());
534 for name in &type_exports {
535 lines.push(format!(" {name},"));
536 }
537 lines.push(format!("}} from '{}';", config.node_package_name()));
538 }
539
540 let custom_mods = config.custom_modules.for_language(Language::Node);
542 for module_name in custom_mods {
543 lines.push(format!("export * from './{module_name}';"));
544 }
545
546 let content = lines.join("\n");
547
548 let output_path = PathBuf::from("packages/typescript/src/index.ts");
550
551 Ok(vec![GeneratedFile {
552 path: output_path,
553 content,
554 generated_header: false,
555 }])
556 }
557
558 fn generate_type_stubs(
559 &self,
560 api: &ApiSurface,
561 config: &ResolvedCrateConfig,
562 ) -> anyhow::Result<Vec<GeneratedFile>> {
563 let prefix = config.node_type_prefix();
564 let exclude_functions: ahash::AHashSet<String> = config
565 .node
566 .as_ref()
567 .map(|c| c.exclude_functions.iter().cloned().collect())
568 .unwrap_or_default();
569 let content = errors::gen_dts(api, &prefix, &exclude_functions, &config.trait_bridges);
570
571 let src_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
576 let crate_root = {
577 let p = PathBuf::from(&src_dir);
578 match p.file_name().and_then(|n| n.to_str()) {
579 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
580 _ => p,
581 }
582 };
583
584 Ok(vec![GeneratedFile {
585 path: crate_root.join("index.d.ts"),
586 content,
587 generated_header: false,
588 }])
589 }
590
591 fn build_config(&self) -> Option<BuildConfig> {
592 Some(BuildConfig {
593 tool: "napi",
594 crate_suffix: "-node",
595 build_dep: BuildDependency::None,
596 post_build: vec![PostBuildStep::PatchFile {
597 path: "index.d.ts",
598 find: "export declare const enum",
599 replace: "export declare enum",
600 }],
601 })
602 }
603}
604
605#[cfg(test)]
607mod tests {
608 use super::NapiBackend;
609 use alef_core::backend::Backend;
610 use alef_core::config::Language;
611
612 #[test]
614 fn napi_backend_name_is_napi() {
615 let b = NapiBackend;
616 assert_eq!(b.name(), "napi");
617 }
618
619 #[test]
621 fn napi_backend_language_is_node() {
622 let b = NapiBackend;
623 assert_eq!(b.language(), Language::Node);
624 }
625}