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<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
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,
33 type_name_prefix: prefix,
35 option_duration_on_defaults: true,
36 opaque_type_names: &[],
37 }
38 }
39}
40
41impl Backend for NapiBackend {
42 fn name(&self) -> &str {
43 "napi"
44 }
45
46 fn language(&self) -> Language {
47 Language::Node
48 }
49
50 fn capabilities(&self) -> Capabilities {
51 Capabilities {
52 supports_async: true,
53 supports_classes: true,
54 supports_enums: true,
55 supports_option: true,
56 supports_result: true,
57 ..Capabilities::default()
58 }
59 }
60
61 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
62 let prefix = config.node_type_prefix();
63 let mapper = NapiMapper::new(prefix.clone());
64 let core_import = config.core_import();
65
66 let output_dir = resolve_output_dir(
68 config.output.node.as_ref(),
69 &config.crate_config.name,
70 "crates/{name}-node/src/",
71 );
72 let has_serde = alef_core::config::detect_serde_available(&output_dir);
73 let cfg = Self::binding_config(&core_import, &prefix, has_serde);
74
75 let mut builder = RustFileBuilder::new().with_generated_header();
76 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
77 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)");
78 builder.add_import("napi::*");
79 builder.add_import("napi_derive::napi");
80
81 builder.add_import("serde_json");
85
86 for trait_path in generators::collect_trait_imports(api) {
88 builder.add_import(&trait_path);
89 }
90
91 let has_maps = api
93 .types
94 .iter()
95 .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
96 || api
97 .functions
98 .iter()
99 .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
100 if has_maps {
101 builder.add_import("std::collections::HashMap");
102 }
103
104 let has_async =
109 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
110
111 if has_async {
112 builder.add_item(&gen_tokio_runtime());
113 }
114
115 let opaque_types: AHashSet<String> = api
117 .types
118 .iter()
119 .filter(|t| t.is_opaque)
120 .map(|t| t.name.clone())
121 .collect();
122 if !opaque_types.is_empty() {
123 builder.add_import("std::sync::Arc");
124 }
125
126 let exclude_types: ahash::AHashSet<String> = config
127 .node
128 .as_ref()
129 .map(|c| c.exclude_types.iter().cloned().collect())
130 .unwrap_or_default();
131
132 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
134
135 for adapter in &config.adapters {
137 match adapter.pattern {
138 alef_core::config::AdapterPattern::Streaming => {
139 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
140 if let Some(struct_code) = adapter_bodies.get(&key) {
141 builder.add_item(struct_code);
142 }
143 }
144 alef_core::config::AdapterPattern::CallbackBridge => {
145 let struct_key = format!("{}.__bridge_struct__", adapter.name);
146 let impl_key = format!("{}.__bridge_impl__", adapter.name);
147 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
148 builder.add_item(struct_code);
149 }
150 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
151 builder.add_item(impl_code);
152 }
153 }
154 _ => {}
155 }
156 }
157
158 for typ in api
162 .types
163 .iter()
164 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
165 {
166 if typ.is_opaque {
167 builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
168 typ, &cfg, &prefix,
169 ));
170 builder.add_item(&gen_opaque_struct_methods(
171 typ,
172 &mapper,
173 &cfg,
174 &opaque_types,
175 &prefix,
176 &adapter_bodies,
177 ));
178 } else {
179 builder.add_item(&gen_struct(typ, &mapper, &prefix, has_serde));
183 }
184 }
185
186 let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
188
189 for enum_def in &api.enums {
190 builder.add_item(&gen_enum(enum_def, &prefix, has_serde));
191 }
192
193 let exclude_functions: ahash::AHashSet<String> = config
194 .node
195 .as_ref()
196 .map(|c| c.exclude_functions.iter().cloned().collect())
197 .unwrap_or_default();
198
199 for func in &api.functions {
200 if exclude_functions.contains(&func.name) {
201 continue;
202 }
203 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
204 if let Some((param_idx, bridge_cfg)) = bridge_param {
205 builder.add_item(&crate::trait_bridge::gen_bridge_function(
206 func,
207 param_idx,
208 bridge_cfg,
209 &mapper,
210 &cfg,
211 &Default::default(),
212 &opaque_types,
213 &core_import,
214 ));
215 } else {
216 builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
217 }
218 }
219
220 for bridge_cfg in &config.trait_bridges {
222 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
223 let bridge_code = crate::trait_bridge::gen_trait_bridge(trait_type, bridge_cfg, &core_import, api);
224 builder.add_item(&bridge_code);
225 }
226 }
227
228 let binding_to_core = alef_codegen::conversions::convertible_types(api);
229 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
230 let input_types = alef_codegen::conversions::input_type_names(api);
231 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
232 type_name_prefix: &prefix,
233 cast_large_ints_to_i64: true,
234 cast_f32_to_f64: true,
235 optionalize_defaults: true,
239 option_duration_on_defaults: true,
240 include_cfg_metadata: true,
241 ..Default::default()
242 };
243 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
245 if input_types.contains(&typ.name)
246 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
247 {
248 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
249 typ,
250 &core_import,
251 &napi_conv_config,
252 ));
253 }
254 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
255 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
256 typ,
257 &core_import,
258 &opaque_types,
259 &napi_conv_config,
260 ));
261 }
262 }
263 for e in &api.enums {
264 let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
265 if is_tagged_data_enum {
266 builder.add_item(&gen_tagged_enum_binding_to_core(
268 e,
269 &core_import,
270 &prefix,
271 &struct_names,
272 ));
273 builder.add_item(&gen_tagged_enum_core_to_binding(
274 e,
275 &core_import,
276 &prefix,
277 &struct_names,
278 ));
279 } else {
280 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
281 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
282 e,
283 &core_import,
284 &napi_conv_config,
285 ));
286 }
287 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
288 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
289 e,
290 &core_import,
291 &napi_conv_config,
292 ));
293 }
294 }
295 }
296
297 for error in &api.errors {
299 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
300 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
301 }
302
303 let content = builder.build();
304
305 let output_dir = resolve_output_dir(
306 config.output.node.as_ref(),
307 &config.crate_config.name,
308 "crates/{name}-node/src/",
309 );
310
311 Ok(vec![GeneratedFile {
312 path: PathBuf::from(&output_dir).join("lib.rs"),
313 content,
314 generated_header: false,
315 }])
316 }
317
318 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
319 let prefix = config.node_type_prefix();
320
321 let mut type_exports = vec![];
323 let mut function_exports = vec![];
324
325 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
327 type_exports.push(format!("{prefix}{}", typ.name));
328 }
329
330 for enum_def in &api.enums {
334 type_exports.push(format!("{prefix}{}", enum_def.name));
335 }
336
337 for func in &api.functions {
342 let js_name = to_node_name(&func.name);
344 function_exports.push(js_name);
345 }
346
347 type_exports.sort();
349 function_exports.sort();
350
351 let mut lines = vec![
354 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
355 "".to_string(),
356 ];
357
358 if !function_exports.is_empty() {
361 lines.push("export {".to_string());
362 for name in &function_exports {
363 lines.push(format!(" {name},"));
364 }
365 lines.push(format!("}} from '{}';", config.node_package_name()));
366 lines.push("".to_string());
367 }
368 if !type_exports.is_empty() {
369 lines.push("export type {".to_string());
370 for name in &type_exports {
371 lines.push(format!(" {name},"));
372 }
373 lines.push(format!("}} from '{}';", config.node_package_name()));
374 }
375
376 let custom_mods = config.custom_modules.for_language(Language::Node);
378 for module_name in custom_mods {
379 lines.push(format!("export * from './{module_name}';"));
380 }
381
382 let content = lines.join("\n");
383
384 let output_path = PathBuf::from("packages/typescript/src/index.ts");
386
387 Ok(vec![GeneratedFile {
388 path: output_path,
389 content,
390 generated_header: false,
391 }])
392 }
393
394 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
395 let prefix = config.node_type_prefix();
396 let content = gen_dts(api, &prefix);
397
398 let src_dir = resolve_output_dir(
403 config.output.node.as_ref(),
404 &config.crate_config.name,
405 "crates/{name}-node/src/",
406 );
407 let crate_root = {
408 let p = PathBuf::from(&src_dir);
409 match p.file_name().and_then(|n| n.to_str()) {
410 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
411 _ => p,
412 }
413 };
414
415 Ok(vec![GeneratedFile {
416 path: crate_root.join("index.d.ts"),
417 content,
418 generated_header: false,
419 }])
420 }
421
422 fn build_config(&self) -> Option<BuildConfig> {
423 Some(BuildConfig {
424 tool: "napi",
425 crate_suffix: "-node",
426 depends_on_ffi: false,
427 post_build: vec![PostBuildStep::PatchFile {
428 path: "index.d.ts",
429 find: "export declare const enum",
430 replace: "export declare enum",
431 }],
432 })
433 }
434}
435
436fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
438 let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
439 struct_builder.add_attr("napi(object)");
441 struct_builder.add_derive("Clone");
442 struct_builder.add_derive("Default");
446 if has_serde {
450 struct_builder.add_derive("serde::Serialize");
451 struct_builder.add_derive("serde::Deserialize");
452 }
453
454 for field in &typ.fields {
455 let mapped_type = mapper.map_type(&field.ty);
456 let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
460 format!("Option<{}>", mapped_type)
461 } else {
462 mapped_type
463 };
464 let js_name = to_node_name(&field.name);
465 let attrs = if js_name != field.name {
466 vec![format!("napi(js_name = \"{}\")", js_name)]
467 } else {
468 vec![]
469 };
470 struct_builder.add_field(&field.name, &field_type, attrs);
471 }
472
473 struct_builder.build()
474}
475
476fn gen_opaque_struct_methods(
478 typ: &TypeDef,
479 mapper: &NapiMapper,
480 cfg: &RustBindingConfig,
481 opaque_types: &AHashSet<String>,
482 prefix: &str,
483 adapter_bodies: &alef_adapters::AdapterBodies,
484) -> String {
485 let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
486 impl_builder.add_attr("napi");
487
488 let (instance, statics) = partition_methods(&typ.methods);
489
490 for method in &instance {
491 impl_builder.add_method(&gen_opaque_instance_method(
492 method,
493 mapper,
494 typ,
495 cfg,
496 opaque_types,
497 prefix,
498 adapter_bodies,
499 ));
500 }
501 for method in &statics {
502 impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
503 }
504
505 impl_builder.build()
506}
507
508fn gen_opaque_instance_method(
510 method: &MethodDef,
511 mapper: &NapiMapper,
512 typ: &TypeDef,
513 cfg: &RustBindingConfig,
514 opaque_types: &AHashSet<String>,
515 prefix: &str,
516 adapter_bodies: &alef_adapters::AdapterBodies,
517) -> String {
518 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
519 let return_type = mapper.map_type(&method.return_type);
520 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
521
522 let js_name = to_node_name(&method.name);
523 let js_name_attr = if js_name != method.name {
524 format!("(js_name = \"{}\")", js_name)
525 } else {
526 String::new()
527 };
528
529 let async_kw = if method.is_async { "async " } else { "" };
530
531 let type_name = &typ.name;
532 let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
533 let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
534 let call_args = napi_gen_call_args(&method.params, opaque_types);
535
536 let opaque_can_delegate = !method.sanitized
539 && !is_ref_mut_receiver
540 && (!is_owned_receiver || typ.is_clone)
541 && method
542 .params
543 .iter()
544 .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
545 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
546
547 let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
548
549 let async_result_wrap = napi_wrap_return(
550 "result",
551 &method.return_type,
552 type_name,
553 opaque_types,
554 true,
555 method.returns_ref,
556 prefix,
557 );
558
559 let adapter_key = format!("{type_name}.{}", method.name);
560 let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
561 adapter_body.clone()
562 } else if !opaque_can_delegate {
563 if cfg.has_serde
565 && !method.sanitized
566 && generators::has_named_params(&method.params, opaque_types)
567 && method.error_type.is_some()
568 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
569 {
570 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
571 let serde_bindings =
572 generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
573 let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
574 let core_call = format!("self.inner.{}({serde_call_args})", method.name);
575 if matches!(method.return_type, TypeRef::Unit) {
576 format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
577 } else {
578 let wrap = napi_wrap_return(
579 "result",
580 &method.return_type,
581 type_name,
582 opaque_types,
583 true,
584 method.returns_ref,
585 prefix,
586 );
587 format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
588 }
589 } else {
590 generators::gen_unimplemented_body(
591 &method.return_type,
592 &format!("{type_name}.{}", method.name),
593 method.error_type.is_some(),
594 cfg,
595 &method.params,
596 opaque_types,
597 )
598 }
599 } else if method.is_async {
600 let inner_clone_line = "let inner = self.inner.clone();\n ";
601 let core_call_str = make_async_core_call(&method.name);
602 generators::gen_async_body(
603 &core_call_str,
604 cfg,
605 method.error_type.is_some(),
606 &async_result_wrap,
607 true,
608 inner_clone_line,
609 matches!(method.return_type, TypeRef::Unit),
610 Some(&return_type),
611 )
612 } else {
613 let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
617 let (let_bindings, call_args_for_call) = if use_let_bindings {
618 let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
619 let args = napi_apply_primitive_casts_to_call_args(
620 &generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
621 &method.params,
622 );
623 (bindings, args)
624 } else {
625 (String::new(), napi_gen_call_args(&method.params, opaque_types))
626 };
627 let core_call = if is_owned_receiver {
628 format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
629 } else {
630 format!("self.inner.{}({})", method.name, call_args_for_call)
631 };
632 if method.error_type.is_some() {
633 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
634 if matches!(method.return_type, TypeRef::Unit) {
635 format!("{let_bindings}{core_call}{err_conv}?;\n Ok(())")
636 } else {
637 let wrap = napi_wrap_return(
638 "result",
639 &method.return_type,
640 type_name,
641 opaque_types,
642 true,
643 method.returns_ref,
644 prefix,
645 );
646 format!("{let_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
647 }
648 } else {
649 format!(
650 "{let_bindings}{}",
651 napi_wrap_return(
652 &core_call,
653 &method.return_type,
654 type_name,
655 opaque_types,
656 true,
657 method.returns_ref,
658 prefix,
659 )
660 )
661 }
662 };
663
664 let mut attrs = String::new();
665 if method.params.len() + 1 > 7 {
667 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
668 }
669 if method.error_type.is_some() {
671 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
672 }
673 if generators::is_trait_method_name(&method.name) {
675 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
676 }
677 format!(
678 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
679 {body}\n}}",
680 method.name
681 )
682}
683
684fn gen_static_method(
686 method: &MethodDef,
687 mapper: &NapiMapper,
688 typ: &TypeDef,
689 cfg: &RustBindingConfig,
690 opaque_types: &AHashSet<String>,
691 prefix: &str,
692) -> String {
693 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
694 let return_type = mapper.map_type(&method.return_type);
695 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
696
697 let js_name = to_node_name(&method.name);
698 let js_name_attr = if js_name != method.name {
699 format!("(js_name = \"{}\")", js_name)
700 } else {
701 String::new()
702 };
703
704 let type_name = &typ.name;
705 let core_type_path = typ.rust_path.replace('-', "_");
706 let call_args = napi_gen_call_args(&method.params, opaque_types);
707 let can_delegate_static = can_auto_delegate(method, opaque_types);
708
709 let async_kw = if method.is_async { "async " } else { "" };
710
711 let body = if !can_delegate_static {
712 generators::gen_unimplemented_body(
713 &method.return_type,
714 &format!("{type_name}::{}", method.name),
715 method.error_type.is_some(),
716 cfg,
717 &method.params,
718 opaque_types,
719 )
720 } else if method.is_async {
721 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
722 let return_wrap = napi_wrap_return(
723 "result",
724 &method.return_type,
725 type_name,
726 opaque_types,
727 typ.is_opaque,
728 method.returns_ref,
729 prefix,
730 );
731 generators::gen_async_body(
732 &core_call,
733 cfg,
734 method.error_type.is_some(),
735 &return_wrap,
736 false,
737 "",
738 matches!(method.return_type, TypeRef::Unit),
739 Some(&return_type),
740 )
741 } else {
742 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
743 if method.error_type.is_some() {
744 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
745 let wrapped = napi_wrap_return(
746 "val",
747 &method.return_type,
748 type_name,
749 opaque_types,
750 typ.is_opaque,
751 method.returns_ref,
752 prefix,
753 );
754 if wrapped == "val" {
755 format!("{core_call}{err_conv}")
756 } else {
757 format!("{core_call}.map(|val| {wrapped}){err_conv}")
758 }
759 } else {
760 napi_wrap_return(
761 &core_call,
762 &method.return_type,
763 type_name,
764 opaque_types,
765 typ.is_opaque,
766 method.returns_ref,
767 prefix,
768 )
769 }
770 };
771
772 let mut attrs = String::new();
773 if method.params.len() > 7 {
775 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
776 }
777 if method.error_type.is_some() {
779 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
780 }
781 if generators::is_trait_method_name(&method.name) {
783 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
784 }
785 format!(
786 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
787 {body}\n}}",
788 method.name
789 )
790}
791
792fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
798 let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
799
800 if is_tagged_data_enum {
801 return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
802 }
803
804 let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
806 "snake_case" => Some("snake_case"),
807 "camelCase" => Some("camelCase"),
808 "kebab-case" => Some("kebab-case"),
809 "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
810 "lowercase" => Some("lowercase"),
811 "UPPERCASE" => Some("UPPERCASE"),
812 "PascalCase" => Some("PascalCase"),
813 _ => None,
814 });
815
816 let string_enum_attr = match napi_case {
817 Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
818 None => "#[napi(string_enum)]".to_string(),
819 };
820
821 let derives = if has_serde {
822 "#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
823 } else {
824 "#[derive(Clone)]".to_string()
825 };
826 let mut lines = vec![
827 string_enum_attr,
828 derives,
829 format!("pub enum {prefix}{} {{", enum_def.name),
830 ];
831
832 for variant in &enum_def.variants {
833 lines.push(format!(" {},", variant.name));
834 }
835
836 lines.push("}".to_string());
837
838 if let Some(first) = enum_def.variants.first() {
840 lines.push(String::new());
841 lines.push("#[allow(clippy::derivable_impls)]".to_string());
842 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
843 lines.push(format!(" fn default() -> Self {{ Self::{} }}", first.name));
844 lines.push("}".to_string());
845 }
846
847 lines.join("\n")
848}
849
850fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
863 use alef_codegen::type_mapper::TypeMapper;
864 let mapper = NapiMapper::new(prefix.to_string());
865
866 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
867
868 let derive = if has_serde {
869 "#[derive(Clone, serde::Serialize, serde::Deserialize)]"
870 } else {
871 "#[derive(Clone)]"
872 };
873 let mut lines = vec![
874 derive.to_string(),
875 "#[napi(object)]".to_string(),
876 format!("pub struct {prefix}{} {{", enum_def.name),
877 format!(" #[napi(js_name = \"{tag_field}\")]"),
878 format!(" pub {tag_field}_tag: String,"),
879 ];
880
881 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
885
886 let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
888 for variant in &enum_def.variants {
889 for field in &variant.fields {
890 if seen_fields.insert(field.name.clone()) {
891 let field_type = if (field.sanitized || mixed_named_fields.contains(&field.name))
894 && matches!(&field.ty, TypeRef::Named(_))
895 {
896 "String".to_string()
897 } else {
898 mapper.map_type(&field.ty).to_string()
899 };
900 let js_name = alef_codegen::naming::to_node_name(&field.name);
901 if js_name != field.name {
902 lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
903 }
904 lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
905 }
906 }
907 }
908
909 lines.push("}".to_string());
910
911 lines.push(String::new());
913 lines.push("#[allow(clippy::derivable_impls)]".to_string());
914 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
915 lines.push(format!(
916 " fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
917 seen_fields
918 .iter()
919 .map(|f| format!("{f}: None"))
920 .collect::<Vec<_>>()
921 .join(", ")
922 ));
923 lines.push("}".to_string());
924
925 lines.join("\n")
926}
927
928fn gen_function(
930 func: &FunctionDef,
931 mapper: &NapiMapper,
932 cfg: &RustBindingConfig,
933 opaque_types: &AHashSet<String>,
934 prefix: &str,
935) -> String {
936 let params = function_params(&func.params, &|ty| {
937 if let TypeRef::Named(n) = ty {
940 if opaque_types.contains(n.as_str()) {
941 return format!("&{prefix}{n}");
942 }
943 }
944 mapper.map_type(ty)
945 });
946 let return_type = mapper.map_type(&func.return_type);
947 let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
948
949 let js_name = to_node_name(&func.name);
950 let js_name_attr = if js_name != func.name {
951 format!("(js_name = \"{}\")", js_name)
952 } else {
953 String::new()
954 };
955
956 let core_import = cfg.core_import;
957 let core_fn_path = {
958 let path = func.rust_path.replace('-', "_");
959 if path.starts_with(core_import) {
960 path
961 } else {
962 format!("{core_import}::{}", func.name)
963 }
964 };
965
966 let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
968 || func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
969 let call_args = if use_let_bindings {
970 let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
971 napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
972 } else {
973 napi_gen_call_args(&func.params, opaque_types)
974 };
975
976 let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
977
978 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
979
980 let async_kw = if func.is_async { "async " } else { "" };
981
982 let body = if !can_delegate_fn {
983 if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
986 let serde_bindings =
987 generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
988 let vec_str_bindings: String = func.params.iter().filter(|p| {
990 p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
991 }).map(|p| {
992 format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n ", p.name, p.name)
993 }).collect();
994 let core_call = format!("{core_fn_path}({call_args})");
995 let await_kw = if func.is_async { ".await" } else { "" };
996
997 if matches!(func.return_type, TypeRef::Unit) {
998 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n Ok(())")
999 } else {
1000 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1001 if wrapped == "val" {
1002 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
1003 } else {
1004 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
1005 }
1006 }
1007 } else {
1008 generators::gen_unimplemented_body(
1009 &func.return_type,
1010 &func.name,
1011 func.error_type.is_some(),
1012 cfg,
1013 &func.params,
1014 opaque_types,
1015 )
1016 }
1017 } else if func.is_async {
1018 let mut let_bindings = if use_let_bindings {
1020 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1021 } else {
1022 String::new()
1023 };
1024 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1026 let core_call = format!("{core_fn_path}({call_args})");
1027 let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
1028 let return_type = mapper.map_type(&func.return_type);
1029 generators::gen_async_body(
1030 &core_call,
1031 cfg,
1032 func.error_type.is_some(),
1033 &return_wrap,
1034 false,
1035 &let_bindings,
1036 matches!(func.return_type, TypeRef::Unit),
1037 Some(&return_type),
1038 )
1039 } else {
1040 let core_call = format!("{core_fn_path}({call_args})");
1041 let mut let_bindings = if use_let_bindings {
1043 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1044 } else {
1045 String::new()
1046 };
1047 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1049
1050 if func.error_type.is_some() {
1051 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1052 if wrapped == "val" {
1053 format!("{let_bindings}{core_call}{err_conv}")
1054 } else {
1055 format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
1056 }
1057 } else {
1058 format!(
1059 "{let_bindings}{}",
1060 napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
1061 )
1062 }
1063 };
1064
1065 let mut attrs = String::new();
1066 if func.params.len() > 7 {
1068 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
1069 }
1070 if func.error_type.is_some() {
1072 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
1073 }
1074 format!(
1075 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
1076 {body}\n}}",
1077 func.name
1078 )
1079}
1080
1081fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
1084 let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
1086 args_list
1087 .iter()
1088 .zip(params.iter())
1089 .map(|(arg, p)| {
1090 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1092 return format!("&{}_f32", p.name);
1093 }
1094 match &p.ty {
1095 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1096 let core_ty = core_prim_str(prim);
1097 if p.optional {
1098 if arg.contains(".map(") || arg.contains(".as_") {
1100 arg.to_string()
1102 } else {
1103 format!("{}.map(|v| v as {})", arg, core_ty)
1104 }
1105 } else {
1106 format!("{} as {}", arg, core_ty)
1108 }
1109 }
1110 _ => arg.to_string(),
1111 }
1112 })
1113 .collect::<Vec<_>>()
1114 .join(", ")
1115}
1116
1117fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
1120 let mut bindings = String::new();
1121 for p in params {
1122 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1123 let conv_name = format!("{}_f32", p.name);
1124 bindings.push_str(&format!(
1125 " let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
1126 p.name
1127 ));
1128 }
1129 }
1130 bindings
1131}
1132
1133fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
1136 params
1137 .iter()
1138 .map(|p| {
1139 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1141 return format!("&{}_f32", p.name);
1142 }
1143 match &p.ty {
1144 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1145 let core_ty = core_prim_str(prim);
1146 if p.optional {
1147 format!("{}.map(|v| v as {})", p.name, core_ty)
1148 } else {
1149 format!("{} as {}", p.name, core_ty)
1150 }
1151 }
1152 TypeRef::Duration => {
1153 if p.optional {
1154 format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
1155 } else {
1156 format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
1157 }
1158 }
1159 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1160 if p.optional {
1161 format!("{}.as_ref().map(|v| &v.inner)", p.name)
1162 } else {
1163 format!("&{}.inner", p.name)
1164 }
1165 }
1166 TypeRef::Named(_) => {
1167 if p.optional {
1168 if p.is_ref {
1169 format!("{}.as_ref()", p.name)
1170 } else {
1171 format!("{}.map(Into::into)", p.name)
1172 }
1173 } else {
1174 format!("{}.into()", p.name)
1175 }
1176 }
1177 TypeRef::String | TypeRef::Char => {
1178 if p.optional {
1179 if p.is_ref {
1180 format!("{}.as_deref()", p.name)
1181 } else {
1182 p.name.clone()
1183 }
1184 } else if p.is_ref {
1185 format!("&{}", p.name)
1186 } else {
1187 p.name.clone()
1188 }
1189 }
1190 TypeRef::Path => {
1191 if p.optional {
1192 if p.is_ref {
1193 format!("{}.as_deref().map(std::path::Path::new)", p.name)
1194 } else {
1195 format!("{}.map(std::path::PathBuf::from)", p.name)
1196 }
1197 } else if p.is_ref {
1198 format!("std::path::Path::new(&{})", p.name)
1199 } else {
1200 format!("std::path::PathBuf::from({})", p.name)
1201 }
1202 }
1203 TypeRef::Bytes => {
1204 if p.optional {
1205 if p.is_ref {
1206 format!("{}.as_deref()", p.name)
1207 } else {
1208 p.name.clone()
1209 }
1210 } else if p.is_ref {
1211 format!("&{}", p.name)
1212 } else {
1213 p.name.clone()
1214 }
1215 }
1216 TypeRef::Vec(_) => {
1217 if p.optional {
1218 if p.is_ref {
1219 format!("{}.as_deref()", p.name)
1220 } else {
1221 p.name.clone()
1222 }
1223 } else if p.is_ref {
1224 format!("&{}", p.name)
1225 } else {
1226 p.name.clone()
1227 }
1228 }
1229 TypeRef::Map(_, _) => {
1230 if p.optional {
1231 if p.is_ref {
1232 format!("{}.as_ref()", p.name)
1233 } else {
1234 p.name.clone()
1235 }
1236 } else if p.is_ref {
1237 format!("&{}", p.name)
1238 } else {
1239 p.name.clone()
1240 }
1241 }
1242 _ => p.name.clone(),
1243 }
1244 })
1245 .collect::<Vec<_>>()
1246 .join(", ")
1247}
1248
1249fn napi_wrap_return(
1252 expr: &str,
1253 return_type: &TypeRef,
1254 type_name: &str,
1255 opaque_types: &AHashSet<String>,
1256 self_is_opaque: bool,
1257 returns_ref: bool,
1258 prefix: &str,
1259) -> String {
1260 match return_type {
1261 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1262 format!("{expr} as i64")
1263 }
1264 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1265 TypeRef::Named(n) if n == type_name && self_is_opaque => {
1267 if returns_ref {
1268 format!("Self {{ inner: Arc::new({expr}.clone()) }}")
1269 } else {
1270 format!("Self {{ inner: Arc::new({expr}) }}")
1271 }
1272 }
1273 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1274 if returns_ref {
1275 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1276 } else {
1277 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1278 }
1279 }
1280 TypeRef::Named(_) => {
1281 if returns_ref {
1282 format!("{expr}.clone().into()")
1283 } else {
1284 format!("{expr}.into()")
1285 }
1286 }
1287 _ => generators::wrap_return(
1288 expr,
1289 return_type,
1290 type_name,
1291 opaque_types,
1292 self_is_opaque,
1293 returns_ref,
1294 false,
1295 ),
1296 }
1297}
1298
1299fn napi_wrap_return_fn(
1301 expr: &str,
1302 return_type: &TypeRef,
1303 opaque_types: &AHashSet<String>,
1304 returns_ref: bool,
1305 prefix: &str,
1306) -> String {
1307 match return_type {
1308 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1309 format!("{expr} as i64")
1310 }
1311 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1312 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1313 if returns_ref {
1314 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1315 } else {
1316 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1317 }
1318 }
1319 TypeRef::Named(_) => {
1320 if returns_ref {
1321 format!("{expr}.clone().into()")
1322 } else {
1323 format!("{expr}.into()")
1324 }
1325 }
1326 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1327 if returns_ref {
1328 format!("{expr}.into()")
1329 } else {
1330 expr.to_string()
1331 }
1332 }
1333 TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1334 TypeRef::Json => format!("{expr}.to_string()"),
1335 TypeRef::Optional(inner) => match inner.as_ref() {
1336 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1337 if returns_ref {
1338 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
1339 } else {
1340 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
1341 }
1342 }
1343 TypeRef::Named(_) => {
1344 if returns_ref {
1345 format!("{expr}.map(|v| v.clone().into())")
1346 } else {
1347 format!("{expr}.map(Into::into)")
1348 }
1349 }
1350 TypeRef::Vec(inner) => match inner.as_ref() {
1351 TypeRef::Named(_) => {
1352 if returns_ref {
1353 format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
1354 } else {
1355 format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
1356 }
1357 }
1358 _ => expr.to_string(),
1359 },
1360 TypeRef::Path => {
1361 format!("{expr}.map(Into::into)")
1362 }
1363 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1364 if returns_ref {
1365 format!("{expr}.map(Into::into)")
1366 } else {
1367 expr.to_string()
1368 }
1369 }
1370 _ => expr.to_string(),
1371 },
1372 TypeRef::Vec(inner) => match inner.as_ref() {
1373 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1374 let target_ty = match p {
1376 alef_core::ir::PrimitiveType::F32 => "f64",
1377 _ => "i64", };
1379 format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
1380 }
1381 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1382 if returns_ref {
1383 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1384 } else {
1385 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
1386 }
1387 }
1388 TypeRef::Named(_) => {
1389 if returns_ref {
1390 format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1391 } else {
1392 format!("{expr}.into_iter().map(Into::into).collect()")
1393 }
1394 }
1395 TypeRef::Path => {
1396 format!("{expr}.into_iter().map(Into::into).collect()")
1397 }
1398 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1399 if returns_ref {
1400 format!("{expr}.into_iter().map(Into::into).collect()")
1401 } else {
1402 expr.to_string()
1403 }
1404 }
1405 _ => expr.to_string(),
1406 },
1407 _ => expr.to_string(),
1408 }
1409}
1410
1411fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
1413 matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
1414}
1415
1416fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1417 matches!(
1421 p,
1422 alef_core::ir::PrimitiveType::U64
1423 | alef_core::ir::PrimitiveType::Usize
1424 | alef_core::ir::PrimitiveType::Isize
1425 | alef_core::ir::PrimitiveType::F32
1426 )
1427}
1428
1429fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1430 match p {
1431 alef_core::ir::PrimitiveType::U64 => "u64",
1432 alef_core::ir::PrimitiveType::Usize => "usize",
1433 alef_core::ir::PrimitiveType::Isize => "isize",
1434 alef_core::ir::PrimitiveType::F32 => "f32",
1435 _ => unreachable!(),
1436 }
1437}
1438
1439fn gen_tokio_runtime() -> String {
1441 "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1442 tokio::runtime::Builder::new_multi_thread()
1443 .enable_all()
1444 .build()
1445 .expect(\"Failed to create Tokio runtime\")
1446});"
1447 .to_string()
1448}
1449
1450fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
1460 let mut lines: Vec<String> = vec![
1461 "/* auto-generated by alef */".to_string(),
1462 "/* eslint-disable */".to_string(),
1463 ];
1464
1465 let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1470 opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1471
1472 let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1474 plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1475
1476 let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1478 sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1479
1480 let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1482 sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1483
1484 enum Decl<'a> {
1487 Class(&'a TypeDef),
1488 Interface(&'a TypeDef),
1489 Enum(&'a EnumDef),
1490 Function(&'a FunctionDef),
1491 }
1492
1493 let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1494 for t in &opaque_types {
1495 all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
1496 }
1497 for t in &plain_types {
1498 all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
1499 }
1500 for e in &sorted_enums {
1501 all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
1502 }
1503 for f in &sorted_fns {
1504 all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1505 }
1506 all_decls.sort_by_key(|a| a.0.to_lowercase());
1507
1508 for (_, decl) in &all_decls {
1509 lines.push(String::new());
1510 match decl {
1511 Decl::Class(typ) => {
1512 lines.extend(format_jsdoc(&typ.doc, ""));
1513 lines.push(format!("export declare class {prefix}{} {{", typ.name));
1514 for method in &typ.methods {
1515 let js_name = to_node_name(&method.name);
1516 let params = dts_params(&method.params, prefix);
1517 let ret = dts_return_type(
1518 &method.return_type,
1519 method.error_type.is_some(),
1520 method.is_async,
1521 prefix,
1522 );
1523 lines.extend(format_jsdoc(&method.doc, " "));
1524 if method.is_static {
1525 lines.push(format!(" static {js_name}({params}): {ret}"));
1526 } else {
1527 lines.push(format!(" {js_name}({params}): {ret}"));
1528 }
1529 }
1530 lines.push("}".to_string());
1531 }
1532 Decl::Interface(typ) => {
1533 lines.extend(format_jsdoc(&typ.doc, ""));
1534 lines.push(format!("export interface {prefix}{} {{", typ.name));
1535 for field in &typ.fields {
1536 let js_name = to_node_name(&field.name);
1537 let ts_ty = dts_type(&field.ty, prefix);
1538 lines.extend(format_jsdoc(&field.doc, " "));
1539 if matches!(field.ty, TypeRef::Optional(_)) {
1542 lines.push(format!(" {js_name}?: {ts_ty}"));
1543 } else {
1544 lines.push(format!(" {js_name}: {ts_ty}"));
1545 }
1546 }
1547 lines.push("}".to_string());
1548 }
1549 Decl::Enum(e) => {
1550 let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
1551 lines.extend(format_jsdoc(&e.doc, ""));
1552 if is_data_enum {
1553 let tag_field = e.serde_tag.as_deref().unwrap_or("type");
1556 let mut member_lines: Vec<String> = Vec::new();
1557 for variant in &e.variants {
1558 let tag_value = variant
1559 .serde_rename
1560 .as_deref()
1561 .map(|s| s.to_string())
1562 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1563 let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
1564 for field in &variant.fields {
1565 let js_name = to_node_name(&field.name);
1566 let ts_ty = dts_type(&field.ty, prefix);
1567 if matches!(field.ty, TypeRef::Optional(_)) {
1568 obj_fields.push(format!("{js_name}?: {ts_ty}"));
1569 } else {
1570 obj_fields.push(format!("{js_name}: {ts_ty}"));
1571 }
1572 }
1573 member_lines.push(format!(" | {{ {} }}", obj_fields.join("; ")));
1574 }
1575 lines.push(format!("export type {prefix}{} =", e.name));
1576 lines.extend(member_lines);
1577 } else {
1578 lines.push(format!("export declare enum {prefix}{} {{", e.name));
1579 for variant in &e.variants {
1580 let value = variant
1583 .serde_rename
1584 .as_deref()
1585 .map(|s| s.to_string())
1586 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1587 lines.extend(format_jsdoc(&variant.doc, " "));
1588 lines.push(format!(" {} = \"{}\",", variant.name, value));
1589 }
1590 lines.push("}".to_string());
1591 }
1592 }
1593 Decl::Function(func) => {
1594 let js_name = to_node_name(&func.name);
1595 let params = dts_params(&func.params, prefix);
1596 let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
1597 lines.extend(format_jsdoc(&func.doc, ""));
1598 lines.push(format!("export declare function {js_name}({params}): {ret};"));
1599 }
1600 }
1601 }
1602
1603 lines.push(String::new());
1604 lines.join("\n")
1605}
1606
1607fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
1613 let doc = doc.trim();
1614 if doc.is_empty() {
1615 return vec![];
1616 }
1617 let lines: Vec<&str> = doc.lines().collect();
1618 if lines.len() == 1 {
1619 vec![format!("{indent}/** {} */", lines[0].trim())]
1620 } else {
1621 let mut out = Vec::with_capacity(lines.len() + 2);
1622 out.push(format!("{indent}/**"));
1623 for line in &lines {
1624 let trimmed = line.trim();
1625 if trimmed.is_empty() {
1626 out.push(format!("{indent} *"));
1627 } else {
1628 out.push(format!("{indent} * {trimmed}"));
1629 }
1630 }
1631 out.push(format!("{indent} */"));
1632 out
1633 }
1634}
1635
1636fn dts_type(ty: &TypeRef, prefix: &str) -> String {
1638 match ty {
1639 TypeRef::Primitive(p) => match p {
1640 alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1641 alef_core::ir::PrimitiveType::U8
1642 | alef_core::ir::PrimitiveType::U16
1643 | alef_core::ir::PrimitiveType::U32
1644 | alef_core::ir::PrimitiveType::I8
1645 | alef_core::ir::PrimitiveType::I16
1646 | alef_core::ir::PrimitiveType::I32
1647 | alef_core::ir::PrimitiveType::F32
1648 | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1649 alef_core::ir::PrimitiveType::U64
1651 | alef_core::ir::PrimitiveType::I64
1652 | alef_core::ir::PrimitiveType::Usize
1653 | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1654 },
1655 TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1656 TypeRef::Bytes => "Uint8Array".to_string(),
1657 TypeRef::Json => "unknown".to_string(),
1658 TypeRef::Duration => "number".to_string(),
1659 TypeRef::Unit => "void".to_string(),
1660 TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
1661 TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
1662 TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
1663 TypeRef::Named(name) => format!("{prefix}{name}"),
1664 }
1665}
1666
1667fn dts_params(params: &[ParamDef], prefix: &str) -> String {
1669 params
1670 .iter()
1671 .map(|p| {
1672 let js_name = to_node_name(&p.name);
1673 let ts_ty = dts_type(&p.ty, prefix);
1674 if p.optional {
1675 format!("{js_name}?: {ts_ty} | undefined | null")
1676 } else {
1677 format!("{js_name}: {ts_ty}")
1678 }
1679 })
1680 .collect::<Vec<_>>()
1681 .join(", ")
1682}
1683
1684fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
1689 let base = match ret {
1690 TypeRef::Unit => "void".to_string(),
1691 other => dts_type(other, prefix),
1692 };
1693 if is_async { format!("Promise<{base}>") } else { base }
1694}
1695
1696fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
1701 match rename_all {
1702 Some("snake_case") => {
1703 let mut out = String::with_capacity(variant_name.len() + 4);
1705 for (i, c) in variant_name.chars().enumerate() {
1706 if c.is_uppercase() && i > 0 {
1707 out.push('_');
1708 }
1709 out.extend(c.to_lowercase());
1710 }
1711 out
1712 }
1713 Some("camelCase") => {
1714 let mut chars = variant_name.chars();
1716 match chars.next() {
1717 None => String::new(),
1718 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1719 }
1720 }
1721 Some("kebab-case") => {
1722 let mut out = String::with_capacity(variant_name.len() + 4);
1723 for (i, c) in variant_name.chars().enumerate() {
1724 if c.is_uppercase() && i > 0 {
1725 out.push('-');
1726 }
1727 out.extend(c.to_lowercase());
1728 }
1729 out
1730 }
1731 Some("SCREAMING_SNAKE_CASE") => {
1732 let mut out = String::with_capacity(variant_name.len() + 4);
1733 for (i, c) in variant_name.chars().enumerate() {
1734 if c.is_uppercase() && i > 0 {
1735 out.push('_');
1736 }
1737 out.extend(c.to_uppercase());
1738 }
1739 out
1740 }
1741 Some("lowercase") => variant_name.to_lowercase(),
1742 Some("UPPERCASE") => variant_name.to_uppercase(),
1743 _ => variant_name.to_string(),
1745 }
1746}
1747
1748fn gen_tagged_enum_binding_to_core(
1750 enum_def: &EnumDef,
1751 core_import: &str,
1752 prefix: &str,
1753 struct_names: &ahash::AHashSet<String>,
1754) -> String {
1755 use alef_core::ir::TypeRef;
1756 use std::fmt::Write;
1757 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1758 let binding_name = format!("{prefix}{}", enum_def.name);
1759 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1760
1761 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1766 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1769
1770 let mut out = String::with_capacity(512);
1771 writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1772 writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
1773 writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
1774
1775 for variant in &enum_def.variants {
1776 let default_tag = variant.name.to_lowercase();
1777 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1778 if variant.fields.is_empty() {
1779 writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
1780 } else {
1781 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1782 let field_exprs: Vec<String> = variant
1783 .fields
1784 .iter()
1785 .map(|f| {
1786 let has_binding = fields_with_binding_struct.contains(f.name.as_str());
1787 let is_mixed = mixed_named_fields.contains(&f.name);
1788 if f.optional {
1789 match &f.ty {
1790 TypeRef::Path => {
1791 format!("val.{}.map(std::path::PathBuf::from)", f.name)
1792 }
1793 TypeRef::Named(n) if is_mixed => {
1794 let core_type = format!("{core_import}::{n}");
1796 format!(
1797 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
1798 f.name
1799 )
1800 }
1801 TypeRef::Named(_) if has_binding => {
1802 format!("val.{}.map(|v| v.into())", f.name)
1803 }
1804 TypeRef::Named(_) => {
1807 format!("val.{}.map(|v| v.into())", f.name)
1808 }
1809 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1810 let core_ty = core_prim_str(p);
1811 format!("val.{}.map(|v| v as {core_ty})", f.name)
1812 }
1813 _ => {
1814 format!("val.{}", f.name)
1815 }
1816 }
1817 } else if f.sanitized {
1818 let expr = "Default::default()".to_string();
1819 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1820 } else {
1821 let expr = match &f.ty {
1822 TypeRef::Named(n) if is_mixed => {
1823 let core_type = format!("{core_import}::{n}");
1825 format!(
1826 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
1827 f.name
1828 )
1829 }
1830 TypeRef::Named(_) if has_binding => {
1831 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1832 }
1833 TypeRef::Named(_) => {
1836 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1837 }
1838 TypeRef::Path => {
1839 format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
1840 }
1841 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1842 let core_ty = core_prim_str(p);
1843 format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
1844 }
1845 _ => {
1846 format!("val.{}.unwrap_or_default()", f.name)
1847 }
1848 };
1849 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1850 }
1851 })
1852 .collect();
1853 if is_tuple {
1854 writeln!(
1855 out,
1856 " \"{tag_value}\" => Self::{}({}),",
1857 variant.name,
1858 field_exprs.join(", ")
1859 )
1860 .ok();
1861 } else {
1862 let field_inits: Vec<String> = variant
1863 .fields
1864 .iter()
1865 .zip(field_exprs.iter())
1866 .map(|(f, expr)| format!("{}: {expr}", f.name))
1867 .collect();
1868 writeln!(
1869 out,
1870 " \"{tag_value}\" => Self::{} {{ {} }},",
1871 variant.name,
1872 field_inits.join(", ")
1873 )
1874 .ok();
1875 }
1876 }
1877 }
1878
1879 if let Some(first) = enum_def.variants.first() {
1881 if first.fields.is_empty() {
1882 writeln!(out, " _ => Self::{},", first.name).ok();
1883 } else {
1884 let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1885 if is_tuple {
1886 let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1887 writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1888 } else {
1889 let defaults: Vec<String> = first
1890 .fields
1891 .iter()
1892 .map(|f| format!("{}: Default::default()", f.name))
1893 .collect();
1894 writeln!(
1895 out,
1896 " _ => Self::{} {{ {} }},",
1897 first.name,
1898 defaults.join(", ")
1899 )
1900 .ok();
1901 }
1902 }
1903 }
1904
1905 writeln!(out, " }}").ok();
1906 writeln!(out, " }}").ok();
1907 write!(out, "}}").ok();
1908 out
1909}
1910
1911fn gen_tagged_enum_core_to_binding(
1913 enum_def: &EnumDef,
1914 core_import: &str,
1915 prefix: &str,
1916 struct_names: &ahash::AHashSet<String>,
1917) -> String {
1918 use std::fmt::Write;
1919 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1920 let binding_name = format!("{prefix}{}", enum_def.name);
1921 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1922 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1923 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1926
1927 let all_fields: Vec<String> = {
1929 let mut fields = std::collections::BTreeSet::new();
1930 for v in &enum_def.variants {
1931 for f in &v.fields {
1932 fields.insert(f.name.clone());
1933 }
1934 }
1935 fields.into_iter().collect()
1936 };
1937
1938 let mut out = String::with_capacity(512);
1939 writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1940 writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
1941 writeln!(out, " match val {{").ok();
1942
1943 for variant in &enum_def.variants {
1944 let default_tag = variant.name.to_lowercase();
1945 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1946 let _variant_field_names: std::collections::BTreeSet<String> =
1947 variant.fields.iter().map(|f| f.name.clone()).collect();
1948
1949 if variant.fields.is_empty() {
1950 writeln!(
1951 out,
1952 " {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1953 variant.name,
1954 all_fields
1955 .iter()
1956 .map(|f| format!("{f}: None"))
1957 .collect::<Vec<_>>()
1958 .join(", ")
1959 )
1960 .ok();
1961 } else {
1962 use alef_core::ir::TypeRef;
1963 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1964 let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
1965 variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1966 let destructured: Vec<String> = variant
1967 .fields
1968 .iter()
1969 .map(|f| {
1970 if f.sanitized {
1971 if is_tuple {
1972 format!("_{}", f.name)
1973 } else {
1974 format!("{}: _{}", f.name, f.name)
1975 }
1976 } else {
1977 f.name.clone()
1978 }
1979 })
1980 .collect();
1981 let field_inits: Vec<String> = all_fields
1982 .iter()
1983 .map(|f| {
1984 if let Some(field) = variant_field_map.get(f.as_str()) {
1985 let has_binding = fields_with_binding_struct.contains(f.as_str());
1986 let is_mixed = mixed_named_fields.contains(f.as_str());
1987 if field.optional {
1988 match &field.ty {
1989 TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
1990 TypeRef::Named(_) if is_mixed => {
1991 format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
1993 }
1994 TypeRef::Named(_) if has_binding => {
1995 format!("{f}: {f}.map(|v| v.into())")
1996 }
1997 TypeRef::Named(_) => {
2000 format!("{f}: {f}.map(|v| v.into())")
2001 }
2002 _ => format!("{f}: {f}"),
2003 }
2004 } else if field.sanitized {
2005 format!("{f}: None")
2006 } else {
2007 match &field.ty {
2008 TypeRef::Named(_) if is_mixed => {
2009 format!("{f}: serde_json::to_string(&{f}).ok()")
2011 }
2012 TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
2013 TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
2016 TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
2017 TypeRef::Primitive(p) if needs_napi_cast(p) => {
2018 match p {
2019 alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
2020 alef_core::ir::PrimitiveType::U64
2021 | alef_core::ir::PrimitiveType::Usize
2022 | alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
2023 _ => format!("{f}: Some({f})"),
2025 }
2026 }
2027 _ => format!("{f}: Some({f})"),
2028 }
2029 }
2030 } else {
2031 format!("{f}: None")
2032 }
2033 })
2034 .collect();
2035 if is_tuple {
2036 writeln!(
2037 out,
2038 " {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2039 variant.name,
2040 destructured.join(", "),
2041 field_inits.join(", ")
2042 )
2043 .ok();
2044 } else {
2045 writeln!(
2046 out,
2047 " {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2048 variant.name,
2049 destructured.join(", "),
2050 field_inits.join(", ")
2051 )
2052 .ok();
2053 }
2054 }
2055 }
2056
2057 writeln!(out, " }}").ok();
2058 writeln!(out, " }}").ok();
2059 write!(out, "}}").ok();
2060 out
2061}
2062
2063fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
2067 use alef_core::ir::TypeRef;
2068 let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
2069
2070 for variant in &enum_def.variants {
2071 for field in &variant.fields {
2072 if field.sanitized {
2073 continue;
2074 }
2075 if let TypeRef::Named(n) = &field.ty {
2076 field_types.entry(&field.name).or_default().insert(n.as_str());
2077 }
2078 }
2079 }
2080
2081 field_types
2082 .into_iter()
2083 .filter(|(_, types)| types.len() > 1)
2084 .map(|(name, _)| name.to_string())
2085 .collect()
2086}
2087
2088fn tagged_enum_binding_struct_fields<'a>(
2094 enum_def: &'a EnumDef,
2095 struct_names: &ahash::AHashSet<String>,
2096) -> ahash::AHashSet<&'a str> {
2097 use alef_core::ir::TypeRef;
2098 let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
2099 let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
2100
2101 for variant in &enum_def.variants {
2102 for field in &variant.fields {
2103 if field.sanitized {
2104 sanitized_fields.insert(&field.name);
2105 }
2106 if let TypeRef::Named(n) = &field.ty {
2107 field_types.entry(&field.name).or_default().push(n);
2108 }
2109 }
2110 }
2111
2112 let mut result = ahash::AHashSet::new();
2113 for (field_name, types) in &field_types {
2114 if sanitized_fields.contains(field_name) {
2115 continue;
2116 }
2117 if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
2119 result.insert(*field_name);
2120 }
2121 }
2122 result
2123}