1use crate::type_map::NapiMapper;
2use ahash::AHashSet;
3use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
4use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
5use alef_codegen::naming::to_node_name;
6use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
7use alef_codegen::type_mapper::TypeMapper;
8use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
11use std::path::PathBuf;
12
13pub struct NapiBackend;
14
15impl NapiBackend {
16 fn binding_config(core_import: &str) -> RustBindingConfig<'_> {
17 RustBindingConfig {
18 struct_attrs: &["napi"],
19 field_attrs: &[],
20 struct_derives: &["Clone"],
21 method_block_attr: Some("napi"),
22 constructor_attr: "#[napi(constructor)]",
23 static_attr: None,
24 function_attr: "#[napi]",
25 enum_attrs: &["napi(string_enum)"],
26 enum_derives: &["Clone"],
27 needs_signature: false,
28 signature_prefix: "",
29 signature_suffix: "",
30 core_import,
31 async_pattern: AsyncPattern::NapiNativeAsync,
32 has_serde: false,
33 type_name_prefix: "Js",
35 option_duration_on_defaults: true,
36 }
37 }
38}
39
40impl Backend for NapiBackend {
41 fn name(&self) -> &str {
42 "napi"
43 }
44
45 fn language(&self) -> Language {
46 Language::Node
47 }
48
49 fn capabilities(&self) -> Capabilities {
50 Capabilities {
51 supports_async: true,
52 supports_classes: true,
53 supports_enums: true,
54 supports_option: true,
55 supports_result: true,
56 ..Capabilities::default()
57 }
58 }
59
60 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
61 let mapper = NapiMapper;
62 let core_import = config.core_import();
63 let cfg = Self::binding_config(&core_import);
64
65 let mut builder = RustFileBuilder::new().with_generated_header();
66 builder.add_inner_attribute("allow(dead_code)");
67 builder.add_import("napi::*");
68 builder.add_import("napi_derive::napi");
69
70 for trait_path in generators::collect_trait_imports(api) {
72 builder.add_import(&trait_path);
73 }
74
75 let has_maps = api
77 .types
78 .iter()
79 .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
80 || api
81 .functions
82 .iter()
83 .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
84 if has_maps {
85 builder.add_import("std::collections::HashMap");
86 }
87
88 let has_async =
93 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
94
95 if has_async {
96 builder.add_item(&gen_tokio_runtime());
97 }
98
99 let opaque_types: AHashSet<String> = api
101 .types
102 .iter()
103 .filter(|t| t.is_opaque)
104 .map(|t| t.name.clone())
105 .collect();
106 if !opaque_types.is_empty() {
107 builder.add_import("std::sync::Arc");
108 }
109
110 for typ in &api.types {
114 if typ.is_opaque {
115 builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, "Js"));
116 builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &cfg, &opaque_types));
117 } else {
118 builder.add_item(&gen_struct(typ, &mapper));
122 }
123 }
124
125 for enum_def in &api.enums {
126 builder.add_item(&gen_enum(enum_def));
127 }
128
129 for func in &api.functions {
130 builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types));
131 }
132
133 let binding_to_core = alef_codegen::conversions::convertible_types(api);
134 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
135 let input_types = alef_codegen::conversions::input_type_names(api);
136 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
137 type_name_prefix: "Js",
138 cast_large_ints_to_i64: true,
139 cast_f32_to_f64: true,
140 optionalize_defaults: true,
144 option_duration_on_defaults: true,
145 include_cfg_metadata: true,
146 ..Default::default()
147 };
148 for typ in &api.types {
150 if input_types.contains(&typ.name)
151 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
152 {
153 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
154 typ,
155 &core_import,
156 &napi_conv_config,
157 ));
158 }
159 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
160 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
161 typ,
162 &core_import,
163 &opaque_types,
164 &napi_conv_config,
165 ));
166 }
167 }
168 for e in &api.enums {
169 let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
170 if is_tagged_data_enum {
171 builder.add_item(&gen_tagged_enum_binding_to_core(e, &core_import));
173 builder.add_item(&gen_tagged_enum_core_to_binding(e, &core_import));
174 } else {
175 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
176 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
177 e,
178 &core_import,
179 &napi_conv_config,
180 ));
181 }
182 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
183 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
184 e,
185 &core_import,
186 &napi_conv_config,
187 ));
188 }
189 }
190 }
191
192 for error in &api.errors {
194 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
195 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
196 }
197
198 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
200
201 let content = builder.build();
202
203 let output_dir = resolve_output_dir(
204 config.output.node.as_ref(),
205 &config.crate_config.name,
206 "crates/{name}-node/src/",
207 );
208
209 Ok(vec![GeneratedFile {
210 path: PathBuf::from(&output_dir).join("lib.rs"),
211 content,
212 generated_header: false,
213 }])
214 }
215
216 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
217 let mut type_exports = vec![];
219 let mut function_exports = vec![];
220
221 for typ in &api.types {
223 type_exports.push(format!("Js{}", typ.name));
224 }
225
226 for enum_def in &api.enums {
230 function_exports.push(format!("Js{}", enum_def.name));
231 }
232
233 for func in &api.functions {
238 let js_name = to_node_name(&func.name);
240 function_exports.push(js_name);
241 }
242
243 type_exports.sort();
245 function_exports.sort();
246
247 let mut lines = vec![
250 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
251 "".to_string(),
252 ];
253
254 if !function_exports.is_empty() {
257 lines.push("export {".to_string());
258 for name in &function_exports {
259 lines.push(format!(" {name},"));
260 }
261 lines.push(format!("}} from '{}';", config.node_package_name()));
262 lines.push("".to_string());
263 }
264 if !type_exports.is_empty() {
265 lines.push("export type {".to_string());
266 for name in &type_exports {
267 lines.push(format!(" {name},"));
268 }
269 lines.push(format!("}} from '{}';", config.node_package_name()));
270 }
271
272 let custom_mods = config.custom_modules.for_language(Language::Node);
274 for module_name in custom_mods {
275 lines.push(format!("export * from './{module_name}';"));
276 }
277
278 let content = lines.join("\n");
279
280 let output_path = PathBuf::from("packages/typescript/src/index.ts");
282
283 Ok(vec![GeneratedFile {
284 path: output_path,
285 content,
286 generated_header: false,
287 }])
288 }
289
290 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
291 let content = gen_dts(api);
292
293 let src_dir = resolve_output_dir(
298 config.output.node.as_ref(),
299 &config.crate_config.name,
300 "crates/{name}-node/src/",
301 );
302 let crate_root = {
303 let p = PathBuf::from(&src_dir);
304 match p.file_name().and_then(|n| n.to_str()) {
305 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
306 _ => p,
307 }
308 };
309
310 Ok(vec![GeneratedFile {
311 path: crate_root.join("index.d.ts"),
312 content,
313 generated_header: false,
314 }])
315 }
316
317 fn build_config(&self) -> Option<BuildConfig> {
318 Some(BuildConfig {
319 tool: "napi",
320 crate_suffix: "-node",
321 depends_on_ffi: false,
322 post_build: vec![PostBuildStep::PatchFile {
323 path: "index.d.ts",
324 find: "export declare const enum",
325 replace: "export declare enum",
326 }],
327 })
328 }
329}
330
331fn gen_struct(typ: &TypeDef, mapper: &NapiMapper) -> String {
333 let mut struct_builder = StructBuilder::new(&format!("Js{}", typ.name));
334 struct_builder.add_attr("napi(object)");
336 struct_builder.add_derive("Clone");
337 if typ.has_default {
339 struct_builder.add_derive("Default");
340 }
341
342 for field in &typ.fields {
343 let mapped_type = mapper.map_type(&field.ty);
344 let field_type = if field.optional || typ.has_default {
347 format!("Option<{}>", mapped_type)
348 } else {
349 mapped_type
350 };
351 let js_name = to_node_name(&field.name);
352 let attrs = if js_name != field.name {
353 vec![format!("napi(js_name = \"{}\")", js_name)]
354 } else {
355 vec![]
356 };
357 struct_builder.add_field(&field.name, &field_type, attrs);
358 }
359
360 struct_builder.build()
361}
362
363fn gen_opaque_struct_methods(
365 typ: &TypeDef,
366 mapper: &NapiMapper,
367 cfg: &RustBindingConfig,
368 opaque_types: &AHashSet<String>,
369) -> String {
370 let mut impl_builder = ImplBuilder::new(&format!("Js{}", typ.name));
371 impl_builder.add_attr("napi");
372
373 let (instance, statics) = partition_methods(&typ.methods);
374
375 for method in &instance {
376 impl_builder.add_method(&gen_opaque_instance_method(method, mapper, typ, cfg, opaque_types));
377 }
378 for method in &statics {
379 impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types));
380 }
381
382 impl_builder.build()
383}
384
385fn gen_opaque_instance_method(
387 method: &MethodDef,
388 mapper: &NapiMapper,
389 typ: &TypeDef,
390 cfg: &RustBindingConfig,
391 opaque_types: &AHashSet<String>,
392) -> String {
393 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
394 let return_type = mapper.map_type(&method.return_type);
395 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
396
397 let js_name = to_node_name(&method.name);
398 let js_name_attr = if js_name != method.name {
399 format!("(js_name = \"{}\")", js_name)
400 } else {
401 String::new()
402 };
403
404 let async_kw = if method.is_async { "async " } else { "" };
405
406 let type_name = &typ.name;
407 let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
408 let call_args = napi_gen_call_args(&method.params, opaque_types);
409
410 let opaque_can_delegate = !method.sanitized
412 && (!is_owned_receiver || typ.is_clone)
413 && method
414 .params
415 .iter()
416 .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
417 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
418
419 let make_core_call = |method_name: &str| -> String {
420 if is_owned_receiver {
421 format!("(*self.inner).clone().{method_name}({call_args})")
422 } else {
423 format!("self.inner.{method_name}({call_args})")
424 }
425 };
426
427 let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
428
429 let async_result_wrap = napi_wrap_return(
430 "result",
431 &method.return_type,
432 type_name,
433 opaque_types,
434 true,
435 method.returns_ref,
436 );
437
438 let body = if !opaque_can_delegate {
439 if cfg.has_serde
441 && !method.sanitized
442 && generators::has_named_params(&method.params, opaque_types)
443 && method.error_type.is_some()
444 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
445 {
446 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
447 let serde_bindings =
448 generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
449 let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
450 let core_call = format!("self.inner.{}({serde_call_args})", method.name);
451 if matches!(method.return_type, TypeRef::Unit) {
452 format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
453 } else {
454 let wrap = napi_wrap_return(
455 "result",
456 &method.return_type,
457 type_name,
458 opaque_types,
459 true,
460 method.returns_ref,
461 );
462 format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
463 }
464 } else {
465 generators::gen_unimplemented_body(
466 &method.return_type,
467 &format!("{type_name}.{}", method.name),
468 method.error_type.is_some(),
469 cfg,
470 &method.params,
471 )
472 }
473 } else if method.is_async {
474 let inner_clone_line = "let inner = self.inner.clone();\n ";
475 let core_call_str = make_async_core_call(&method.name);
476 generators::gen_async_body(
477 &core_call_str,
478 cfg,
479 method.error_type.is_some(),
480 &async_result_wrap,
481 true,
482 inner_clone_line,
483 matches!(method.return_type, TypeRef::Unit),
484 )
485 } else {
486 let core_call = make_core_call(&method.name);
487 if method.error_type.is_some() {
488 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
489 if matches!(method.return_type, TypeRef::Unit) {
490 format!("{core_call}{err_conv}?;\n Ok(())")
491 } else {
492 let wrap = napi_wrap_return(
493 "result",
494 &method.return_type,
495 type_name,
496 opaque_types,
497 true,
498 method.returns_ref,
499 );
500 format!("let result = {core_call}{err_conv}?;\n Ok({wrap})")
501 }
502 } else {
503 napi_wrap_return(
504 &core_call,
505 &method.return_type,
506 type_name,
507 opaque_types,
508 true,
509 method.returns_ref,
510 )
511 }
512 };
513
514 let mut attrs = String::new();
515 if method.params.len() + 1 > 7 {
517 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
518 }
519 if method.error_type.is_some() {
521 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
522 }
523 if generators::is_trait_method_name(&method.name) {
525 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
526 }
527 format!(
528 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
529 {body}\n}}",
530 method.name
531 )
532}
533
534fn gen_static_method(
536 method: &MethodDef,
537 mapper: &NapiMapper,
538 typ: &TypeDef,
539 cfg: &RustBindingConfig,
540 opaque_types: &AHashSet<String>,
541) -> String {
542 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
543 let return_type = mapper.map_type(&method.return_type);
544 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
545
546 let js_name = to_node_name(&method.name);
547 let js_name_attr = if js_name != method.name {
548 format!("(js_name = \"{}\")", js_name)
549 } else {
550 String::new()
551 };
552
553 let type_name = &typ.name;
554 let core_type_path = typ.rust_path.replace('-', "_");
555 let call_args = napi_gen_call_args(&method.params, opaque_types);
556 let can_delegate_static = can_auto_delegate(method, opaque_types);
557
558 let async_kw = if method.is_async { "async " } else { "" };
559
560 let body = if !can_delegate_static {
561 generators::gen_unimplemented_body(
562 &method.return_type,
563 &format!("{type_name}::{}", method.name),
564 method.error_type.is_some(),
565 cfg,
566 &method.params,
567 )
568 } else if method.is_async {
569 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
570 let return_wrap = napi_wrap_return(
571 "result",
572 &method.return_type,
573 type_name,
574 opaque_types,
575 typ.is_opaque,
576 method.returns_ref,
577 );
578 generators::gen_async_body(
579 &core_call,
580 cfg,
581 method.error_type.is_some(),
582 &return_wrap,
583 false,
584 "",
585 matches!(method.return_type, TypeRef::Unit),
586 )
587 } else {
588 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
589 if method.error_type.is_some() {
590 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
591 let wrapped = napi_wrap_return(
592 "val",
593 &method.return_type,
594 type_name,
595 opaque_types,
596 typ.is_opaque,
597 method.returns_ref,
598 );
599 if wrapped == "val" {
600 format!("{core_call}{err_conv}")
601 } else {
602 format!("{core_call}.map(|val| {wrapped}){err_conv}")
603 }
604 } else {
605 napi_wrap_return(
606 &core_call,
607 &method.return_type,
608 type_name,
609 opaque_types,
610 typ.is_opaque,
611 method.returns_ref,
612 )
613 }
614 };
615
616 let mut attrs = String::new();
617 if method.params.len() > 7 {
619 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
620 }
621 if method.error_type.is_some() {
623 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
624 }
625 if generators::is_trait_method_name(&method.name) {
627 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
628 }
629 format!(
630 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
631 {body}\n}}",
632 method.name
633 )
634}
635
636fn gen_enum(enum_def: &EnumDef) -> String {
642 let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
643
644 if is_tagged_data_enum {
645 return gen_tagged_enum_as_object(enum_def);
646 }
647
648 let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
650 "snake_case" => Some("snake_case"),
651 "camelCase" => Some("camelCase"),
652 "kebab-case" => Some("kebab-case"),
653 "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
654 "lowercase" => Some("lowercase"),
655 "UPPERCASE" => Some("UPPERCASE"),
656 "PascalCase" => Some("PascalCase"),
657 _ => None,
658 });
659
660 let string_enum_attr = match napi_case {
661 Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
662 None => "#[napi(string_enum)]".to_string(),
663 };
664
665 let mut lines = vec![
666 string_enum_attr,
667 "#[derive(Clone)]".to_string(),
668 format!("pub enum Js{} {{", enum_def.name),
669 ];
670
671 for variant in &enum_def.variants {
672 lines.push(format!(" {},", variant.name));
673 }
674
675 lines.push("}".to_string());
676
677 if let Some(first) = enum_def.variants.first() {
679 lines.push(String::new());
680 lines.push("#[allow(clippy::derivable_impls)]".to_string());
681 lines.push(format!("impl Default for Js{} {{", enum_def.name));
682 lines.push(format!(" fn default() -> Self {{ Self::{} }}", first.name));
683 lines.push("}".to_string());
684 }
685
686 lines.join("\n")
687}
688
689fn gen_tagged_enum_as_object(enum_def: &EnumDef) -> String {
702 use alef_codegen::type_mapper::TypeMapper;
703 let mapper = NapiMapper;
704
705 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
706
707 let mut lines = vec![
708 "#[derive(Clone)]".to_string(),
709 "#[napi(object)]".to_string(),
710 format!("pub struct Js{} {{", enum_def.name),
711 format!(" #[napi(js_name = \"{tag_field}\")]"),
712 format!(" pub {tag_field}_tag: String,"),
713 ];
714
715 let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
717 for variant in &enum_def.variants {
718 for field in &variant.fields {
719 if seen_fields.insert(field.name.clone()) {
720 let field_type = mapper.map_type(&field.ty);
721 let js_name = alef_codegen::naming::to_node_name(&field.name);
722 if js_name != field.name {
723 lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
724 }
725 lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
726 }
727 }
728 }
729
730 lines.push("}".to_string());
731
732 lines.push(String::new());
734 lines.push("#[allow(clippy::derivable_impls)]".to_string());
735 lines.push(format!("impl Default for Js{} {{", enum_def.name));
736 lines.push(format!(
737 " fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
738 seen_fields
739 .iter()
740 .map(|f| format!("{f}: None"))
741 .collect::<Vec<_>>()
742 .join(", ")
743 ));
744 lines.push("}".to_string());
745
746 lines.join("\n")
747}
748
749fn gen_function(
751 func: &FunctionDef,
752 mapper: &NapiMapper,
753 cfg: &RustBindingConfig,
754 opaque_types: &AHashSet<String>,
755) -> String {
756 let params = function_params(&func.params, &|ty| {
757 if let TypeRef::Named(n) = ty {
760 if opaque_types.contains(n.as_str()) {
761 return format!("&Js{n}");
762 }
763 }
764 mapper.map_type(ty)
765 });
766 let return_type = mapper.map_type(&func.return_type);
767 let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
768
769 let js_name = to_node_name(&func.name);
770 let js_name_attr = if js_name != func.name {
771 format!("(js_name = \"{}\")", js_name)
772 } else {
773 String::new()
774 };
775
776 let core_import = cfg.core_import;
777 let core_fn_path = {
778 let path = func.rust_path.replace('-', "_");
779 if path.starts_with(core_import) {
780 path
781 } else {
782 format!("{core_import}::{}", func.name)
783 }
784 };
785
786 let use_let_bindings = generators::has_named_params(&func.params, opaque_types);
788 let call_args = if use_let_bindings {
789 generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
790 } else {
791 napi_gen_call_args(&func.params, opaque_types)
792 };
793
794 let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
795
796 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
797
798 let async_kw = if func.is_async { "async " } else { "" };
799
800 let body = if !can_delegate_fn {
801 if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
803 let serde_bindings =
804 generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
805 let core_call = format!("{core_fn_path}({call_args})");
806
807 if matches!(func.return_type, TypeRef::Unit) {
808 format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
809 } else {
810 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
811 if wrapped == "val" {
812 format!("{serde_bindings}{core_call}{err_conv}")
813 } else {
814 format!("{serde_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
815 }
816 }
817 } else {
818 generators::gen_unimplemented_body(
819 &func.return_type,
820 &func.name,
821 func.error_type.is_some(),
822 cfg,
823 &func.params,
824 )
825 }
826 } else if func.is_async {
827 let core_call = format!("{core_fn_path}({call_args})");
828 let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref);
829 generators::gen_async_body(
830 &core_call,
831 cfg,
832 func.error_type.is_some(),
833 &return_wrap,
834 false,
835 "",
836 matches!(func.return_type, TypeRef::Unit),
837 )
838 } else {
839 let core_call = format!("{core_fn_path}({call_args})");
840 let let_bindings = if use_let_bindings {
842 generators::gen_named_let_bindings_pub(&func.params, opaque_types)
843 } else {
844 String::new()
845 };
846
847 if func.error_type.is_some() {
848 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
849 if wrapped == "val" {
850 format!("{let_bindings}{core_call}{err_conv}")
851 } else {
852 format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
853 }
854 } else {
855 format!(
856 "{let_bindings}{}",
857 napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref)
858 )
859 }
860 };
861
862 let mut attrs = String::new();
863 if func.params.len() > 7 {
865 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
866 }
867 if func.error_type.is_some() {
869 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
870 }
871 format!(
872 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
873 {body}\n}}",
874 func.name
875 )
876}
877
878fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
880 params
881 .iter()
882 .map(|p| match &p.ty {
883 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
884 let core_ty = core_prim_str(prim);
885 if p.optional {
886 format!("{}.map(|v| v as {})", p.name, core_ty)
887 } else {
888 format!("{} as {}", p.name, core_ty)
889 }
890 }
891 TypeRef::Duration => {
892 if p.optional {
893 format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
894 } else {
895 format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
896 }
897 }
898 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
899 if p.optional {
900 format!("{}.as_ref().map(|v| &v.inner)", p.name)
901 } else {
902 format!("&{}.inner", p.name)
903 }
904 }
905 TypeRef::Named(_) => {
906 if p.optional {
907 format!("{}.map(Into::into)", p.name)
908 } else {
909 format!("{}.into()", p.name)
910 }
911 }
912 TypeRef::String | TypeRef::Char => format!("&{}", p.name),
913 TypeRef::Path => format!("std::path::PathBuf::from({})", p.name),
914 TypeRef::Bytes => format!("&{}", p.name),
915 _ => p.name.clone(),
916 })
917 .collect::<Vec<_>>()
918 .join(", ")
919}
920
921fn napi_wrap_return(
924 expr: &str,
925 return_type: &TypeRef,
926 type_name: &str,
927 opaque_types: &AHashSet<String>,
928 self_is_opaque: bool,
929 returns_ref: bool,
930) -> String {
931 match return_type {
932 TypeRef::Primitive(p) if needs_napi_cast(p) => {
933 format!("{expr} as i64")
934 }
935 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
936 TypeRef::Named(n) if n == type_name && self_is_opaque => {
938 if returns_ref {
939 format!("Self {{ inner: Arc::new({expr}.clone()) }}")
940 } else {
941 format!("Self {{ inner: Arc::new({expr}) }}")
942 }
943 }
944 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
945 if returns_ref {
946 format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
947 } else {
948 format!("Js{n} {{ inner: Arc::new({expr}) }}")
949 }
950 }
951 TypeRef::Named(_) => {
952 if returns_ref {
953 format!("{expr}.clone().into()")
954 } else {
955 format!("{expr}.into()")
956 }
957 }
958 _ => generators::wrap_return(
959 expr,
960 return_type,
961 type_name,
962 opaque_types,
963 self_is_opaque,
964 returns_ref,
965 false,
966 ),
967 }
968}
969
970fn napi_wrap_return_fn(
972 expr: &str,
973 return_type: &TypeRef,
974 opaque_types: &AHashSet<String>,
975 returns_ref: bool,
976) -> String {
977 match return_type {
978 TypeRef::Primitive(p) if needs_napi_cast(p) => {
979 format!("{expr} as i64")
980 }
981 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
982 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
983 if returns_ref {
984 format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
985 } else {
986 format!("Js{n} {{ inner: Arc::new({expr}) }}")
987 }
988 }
989 TypeRef::Named(_) => {
990 if returns_ref {
991 format!("{expr}.clone().into()")
992 } else {
993 format!("{expr}.into()")
994 }
995 }
996 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
997 if returns_ref {
998 format!("{expr}.into()")
999 } else {
1000 expr.to_string()
1001 }
1002 }
1003 TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1004 TypeRef::Json => format!("{expr}.to_string()"),
1005 TypeRef::Optional(inner) => match inner.as_ref() {
1006 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1007 if returns_ref {
1008 format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v.clone()) }})")
1009 } else {
1010 format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v) }})")
1011 }
1012 }
1013 TypeRef::Named(_) => {
1014 if returns_ref {
1015 format!("{expr}.map(|v| v.clone().into())")
1016 } else {
1017 format!("{expr}.map(Into::into)")
1018 }
1019 }
1020 TypeRef::Path => {
1021 format!("{expr}.map(Into::into)")
1022 }
1023 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1024 if returns_ref {
1025 format!("{expr}.map(Into::into)")
1026 } else {
1027 expr.to_string()
1028 }
1029 }
1030 _ => expr.to_string(),
1031 },
1032 TypeRef::Vec(inner) => match inner.as_ref() {
1033 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1034 if returns_ref {
1035 format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1036 } else {
1037 format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v) }}).collect()")
1038 }
1039 }
1040 TypeRef::Named(_) => {
1041 if returns_ref {
1042 format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1043 } else {
1044 format!("{expr}.into_iter().map(Into::into).collect()")
1045 }
1046 }
1047 TypeRef::Path => {
1048 format!("{expr}.into_iter().map(Into::into).collect()")
1049 }
1050 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1051 if returns_ref {
1052 format!("{expr}.into_iter().map(Into::into).collect()")
1053 } else {
1054 expr.to_string()
1055 }
1056 }
1057 _ => expr.to_string(),
1058 },
1059 _ => expr.to_string(),
1060 }
1061}
1062
1063fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1064 matches!(
1065 p,
1066 alef_core::ir::PrimitiveType::U64 | alef_core::ir::PrimitiveType::Usize | alef_core::ir::PrimitiveType::Isize
1067 )
1068}
1069
1070fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1071 match p {
1072 alef_core::ir::PrimitiveType::U64 => "u64",
1073 alef_core::ir::PrimitiveType::Usize => "usize",
1074 alef_core::ir::PrimitiveType::Isize => "isize",
1075 _ => unreachable!(),
1076 }
1077}
1078
1079fn gen_tokio_runtime() -> String {
1081 "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1082 tokio::runtime::Builder::new_multi_thread()
1083 .enable_all()
1084 .build()
1085 .expect(\"Failed to create Tokio runtime\")
1086});"
1087 .to_string()
1088}
1089
1090fn gen_dts(api: &ApiSurface) -> String {
1100 let mut lines: Vec<String> = vec![
1101 "/* auto-generated by alef */".to_string(),
1102 "/* eslint-disable */".to_string(),
1103 ];
1104
1105 let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1110 opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1111
1112 let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1114 plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1115
1116 let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1118 sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1119
1120 let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1122 sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1123
1124 enum Decl<'a> {
1127 Class(&'a TypeDef),
1128 Interface(&'a TypeDef),
1129 Enum(&'a EnumDef),
1130 Function(&'a FunctionDef),
1131 }
1132
1133 let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1134 for t in &opaque_types {
1135 all_decls.push((format!("Js{}", t.name), Decl::Class(t)));
1136 }
1137 for t in &plain_types {
1138 all_decls.push((format!("Js{}", t.name), Decl::Interface(t)));
1139 }
1140 for e in &sorted_enums {
1141 all_decls.push((format!("Js{}", e.name), Decl::Enum(e)));
1142 }
1143 for f in &sorted_fns {
1144 all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1145 }
1146 all_decls.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
1147
1148 for (_, decl) in &all_decls {
1149 lines.push(String::new());
1150 match decl {
1151 Decl::Class(typ) => {
1152 lines.push(format!("export declare class Js{} {{", typ.name));
1153 for method in &typ.methods {
1154 let js_name = to_node_name(&method.name);
1155 let params = dts_params(&method.params);
1156 let ret = dts_return_type(&method.return_type, method.error_type.is_some(), method.is_async);
1157 if method.is_static {
1158 lines.push(format!(" static {js_name}({params}): {ret}"));
1159 } else {
1160 lines.push(format!(" {js_name}({params}): {ret}"));
1161 }
1162 }
1163 lines.push("}".to_string());
1164 }
1165 Decl::Interface(typ) => {
1166 lines.push(format!("export interface Js{} {{", typ.name));
1167 for field in &typ.fields {
1168 let js_name = to_node_name(&field.name);
1169 let ts_ty = dts_type(&field.ty);
1170 lines.push(format!(" {js_name}?: {ts_ty}"));
1172 }
1173 lines.push("}".to_string());
1174 }
1175 Decl::Enum(e) => {
1176 lines.push(format!("export declare enum Js{} {{", e.name));
1177 for variant in &e.variants {
1178 let value = variant.serde_rename.as_deref().unwrap_or(variant.name.as_str());
1180 lines.push(format!(" {} = '{}',", variant.name, value));
1181 }
1182 lines.push("}".to_string());
1183 }
1184 Decl::Function(func) => {
1185 let js_name = to_node_name(&func.name);
1186 let params = dts_params(&func.params);
1187 let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async);
1188 lines.push(format!("export declare function {js_name}({params}): {ret}"));
1189 }
1190 }
1191 }
1192
1193 lines.push(String::new());
1194 lines.join("\n")
1195}
1196
1197fn dts_type(ty: &TypeRef) -> String {
1199 match ty {
1200 TypeRef::Primitive(p) => match p {
1201 alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1202 alef_core::ir::PrimitiveType::U8
1203 | alef_core::ir::PrimitiveType::U16
1204 | alef_core::ir::PrimitiveType::U32
1205 | alef_core::ir::PrimitiveType::I8
1206 | alef_core::ir::PrimitiveType::I16
1207 | alef_core::ir::PrimitiveType::I32
1208 | alef_core::ir::PrimitiveType::F32
1209 | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1210 alef_core::ir::PrimitiveType::U64
1212 | alef_core::ir::PrimitiveType::I64
1213 | alef_core::ir::PrimitiveType::Usize
1214 | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1215 },
1216 TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1217 TypeRef::Bytes => "Uint8Array".to_string(),
1218 TypeRef::Json => "string".to_string(),
1219 TypeRef::Duration => "number".to_string(),
1220 TypeRef::Unit => "void".to_string(),
1221 TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner)),
1222 TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner)),
1223 TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k), dts_type(v)),
1224 TypeRef::Named(name) => format!("Js{name}"),
1225 }
1226}
1227
1228fn dts_params(params: &[ParamDef]) -> String {
1230 params
1231 .iter()
1232 .map(|p| {
1233 let js_name = to_node_name(&p.name);
1234 let ts_ty = dts_type(&p.ty);
1235 if p.optional {
1236 format!("{js_name}?: {ts_ty} | undefined | null")
1237 } else {
1238 format!("{js_name}: {ts_ty}")
1239 }
1240 })
1241 .collect::<Vec<_>>()
1242 .join(", ")
1243}
1244
1245fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool) -> String {
1250 let base = match ret {
1251 TypeRef::Unit => "void".to_string(),
1252 other => dts_type(other),
1253 };
1254 if is_async { format!("Promise<{base}>") } else { base }
1255}
1256
1257fn gen_tagged_enum_binding_to_core(enum_def: &EnumDef, core_import: &str) -> String {
1259 use alef_core::ir::TypeRef;
1260 use std::fmt::Write;
1261 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1262 let binding_name = format!("Js{}", enum_def.name);
1263 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1264
1265 let mut out = String::with_capacity(512);
1266 writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1267 writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
1268 writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
1269
1270 for variant in &enum_def.variants {
1271 let default_tag = variant.name.to_lowercase();
1272 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1273 if variant.fields.is_empty() {
1274 writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
1275 } else {
1276 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1277 let field_exprs: Vec<String> = variant
1278 .fields
1279 .iter()
1280 .map(|f| {
1281 if f.optional {
1282 format!("val.{}", f.name)
1283 } else if f.sanitized {
1284 "Default::default()".to_string()
1285 } else {
1286 match &f.ty {
1287 TypeRef::Named(_) => {
1288 format!("val.{}.unwrap_or_default().into()", f.name)
1289 }
1290 _ => {
1291 format!("val.{}.unwrap_or_default()", f.name)
1292 }
1293 }
1294 }
1295 })
1296 .collect();
1297 if is_tuple {
1298 writeln!(
1299 out,
1300 " \"{tag_value}\" => Self::{}({}),",
1301 variant.name,
1302 field_exprs.join(", ")
1303 )
1304 .ok();
1305 } else {
1306 let field_inits: Vec<String> = variant
1307 .fields
1308 .iter()
1309 .zip(field_exprs.iter())
1310 .map(|(f, expr)| format!("{}: {expr}", f.name))
1311 .collect();
1312 writeln!(
1313 out,
1314 " \"{tag_value}\" => Self::{} {{ {} }},",
1315 variant.name,
1316 field_inits.join(", ")
1317 )
1318 .ok();
1319 }
1320 }
1321 }
1322
1323 if let Some(first) = enum_def.variants.first() {
1325 if first.fields.is_empty() {
1326 writeln!(out, " _ => Self::{},", first.name).ok();
1327 } else {
1328 let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1329 if is_tuple {
1330 let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1331 writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1332 } else {
1333 let defaults: Vec<String> = first
1334 .fields
1335 .iter()
1336 .map(|f| format!("{}: Default::default()", f.name))
1337 .collect();
1338 writeln!(
1339 out,
1340 " _ => Self::{} {{ {} }},",
1341 first.name,
1342 defaults.join(", ")
1343 )
1344 .ok();
1345 }
1346 }
1347 }
1348
1349 writeln!(out, " }}").ok();
1350 writeln!(out, " }}").ok();
1351 write!(out, "}}").ok();
1352 out
1353}
1354
1355fn gen_tagged_enum_core_to_binding(enum_def: &EnumDef, core_import: &str) -> String {
1357 use std::fmt::Write;
1358 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1359 let binding_name = format!("Js{}", enum_def.name);
1360 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1361
1362 let all_fields: Vec<String> = {
1364 let mut fields = std::collections::BTreeSet::new();
1365 for v in &enum_def.variants {
1366 for f in &v.fields {
1367 fields.insert(f.name.clone());
1368 }
1369 }
1370 fields.into_iter().collect()
1371 };
1372
1373 let mut out = String::with_capacity(512);
1374 writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1375 writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
1376 writeln!(out, " match val {{").ok();
1377
1378 for variant in &enum_def.variants {
1379 let default_tag = variant.name.to_lowercase();
1380 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1381 let _variant_field_names: std::collections::BTreeSet<String> =
1382 variant.fields.iter().map(|f| f.name.clone()).collect();
1383
1384 if variant.fields.is_empty() {
1385 writeln!(
1386 out,
1387 " {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1388 variant.name,
1389 all_fields
1390 .iter()
1391 .map(|f| format!("{f}: None"))
1392 .collect::<Vec<_>>()
1393 .join(", ")
1394 )
1395 .ok();
1396 } else {
1397 use alef_core::ir::TypeRef;
1398 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1399 let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
1400 variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1401 let destructured: Vec<String> = variant
1402 .fields
1403 .iter()
1404 .map(|f| {
1405 if f.sanitized {
1406 if is_tuple {
1407 format!("_{}", f.name)
1408 } else {
1409 format!("{}: _{}", f.name, f.name)
1410 }
1411 } else {
1412 f.name.clone()
1413 }
1414 })
1415 .collect();
1416 let field_inits: Vec<String> = all_fields
1417 .iter()
1418 .map(|f| {
1419 if let Some(field) = variant_field_map.get(f.as_str()) {
1420 if field.optional {
1421 format!("{f}: {f}")
1422 } else if field.sanitized {
1423 format!("{f}: None")
1424 } else {
1425 match &field.ty {
1426 TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
1427 _ => format!("{f}: Some({f})"),
1428 }
1429 }
1430 } else {
1431 format!("{f}: None")
1432 }
1433 })
1434 .collect();
1435 if is_tuple {
1436 writeln!(
1437 out,
1438 " {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1439 variant.name,
1440 destructured.join(", "),
1441 field_inits.join(", ")
1442 )
1443 .ok();
1444 } else {
1445 writeln!(
1446 out,
1447 " {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1448 variant.name,
1449 destructured.join(", "),
1450 field_inits.join(", ")
1451 )
1452 .ok();
1453 }
1454 }
1455 }
1456
1457 writeln!(out, " }}").ok();
1458 writeln!(out, " }}").ok();
1459 write!(out, "}}").ok();
1460 out
1461}