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