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