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, BuildDependency, 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 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
205 if func.sanitized && bridge_param.is_none() {
210 continue;
211 }
212 if let Some((param_idx, bridge_cfg)) = bridge_param {
213 builder.add_item(&crate::trait_bridge::gen_bridge_function(
214 func,
215 param_idx,
216 bridge_cfg,
217 &mapper,
218 &cfg,
219 &Default::default(),
220 &opaque_types,
221 &core_import,
222 ));
223 } else {
224 builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
225 }
226 }
227
228 for bridge_cfg in &config.trait_bridges {
230 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
231 let bridge = crate::trait_bridge::gen_trait_bridge(
232 trait_type,
233 bridge_cfg,
234 &core_import,
235 &config.error_type(),
236 &config.error_constructor(),
237 api,
238 );
239 for imp in &bridge.imports {
240 builder.add_import(imp);
241 }
242 builder.add_item(&bridge.code);
243 }
244 }
245
246 let binding_to_core = alef_codegen::conversions::convertible_types(api);
247 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
248 let input_types = alef_codegen::conversions::input_type_names(api);
249 let napi_conv_config = alef_codegen::conversions::ConversionConfig {
250 type_name_prefix: &prefix,
251 cast_large_ints_to_i64: true,
252 cast_f32_to_f64: true,
253 optionalize_defaults: true,
257 option_duration_on_defaults: true,
258 include_cfg_metadata: true,
259 ..Default::default()
260 };
261 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
263 if input_types.contains(&typ.name)
264 && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
265 {
266 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
267 typ,
268 &core_import,
269 &napi_conv_config,
270 ));
271 }
272 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
273 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
274 typ,
275 &core_import,
276 &opaque_types,
277 &napi_conv_config,
278 ));
279 }
280 }
281 for e in &api.enums {
282 let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
283 if is_tagged_data_enum {
284 builder.add_item(&gen_tagged_enum_binding_to_core(
286 e,
287 &core_import,
288 &prefix,
289 &struct_names,
290 ));
291 builder.add_item(&gen_tagged_enum_core_to_binding(
292 e,
293 &core_import,
294 &prefix,
295 &struct_names,
296 ));
297 } else {
298 if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
299 builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
300 e,
301 &core_import,
302 &napi_conv_config,
303 ));
304 }
305 if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
306 builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
307 e,
308 &core_import,
309 &napi_conv_config,
310 ));
311 }
312 }
313 }
314
315 for error in &api.errors {
317 builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
318 builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
319 }
320
321 let content = builder.build();
322
323 let output_dir = resolve_output_dir(
324 config.output.node.as_ref(),
325 &config.crate_config.name,
326 "crates/{name}-node/src/",
327 );
328
329 Ok(vec![GeneratedFile {
330 path: PathBuf::from(&output_dir).join("lib.rs"),
331 content,
332 generated_header: false,
333 }])
334 }
335
336 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
337 let prefix = config.node_type_prefix();
338
339 let mut type_exports = vec![];
341 let mut function_exports = vec![];
342
343 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
345 type_exports.push(format!("{prefix}{}", typ.name));
346 }
347
348 for enum_def in &api.enums {
352 type_exports.push(format!("{prefix}{}", enum_def.name));
353 }
354
355 for func in &api.functions {
360 let js_name = to_node_name(&func.name);
362 function_exports.push(js_name);
363 }
364
365 type_exports.sort();
367 function_exports.sort();
368
369 let mut lines = vec![
372 "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
373 "".to_string(),
374 ];
375
376 if !function_exports.is_empty() {
379 lines.push("export {".to_string());
380 for name in &function_exports {
381 lines.push(format!(" {name},"));
382 }
383 lines.push(format!("}} from '{}';", config.node_package_name()));
384 lines.push("".to_string());
385 }
386 if !type_exports.is_empty() {
387 lines.push("export type {".to_string());
388 for name in &type_exports {
389 lines.push(format!(" {name},"));
390 }
391 lines.push(format!("}} from '{}';", config.node_package_name()));
392 }
393
394 let custom_mods = config.custom_modules.for_language(Language::Node);
396 for module_name in custom_mods {
397 lines.push(format!("export * from './{module_name}';"));
398 }
399
400 let content = lines.join("\n");
401
402 let output_path = PathBuf::from("packages/typescript/src/index.ts");
404
405 Ok(vec![GeneratedFile {
406 path: output_path,
407 content,
408 generated_header: false,
409 }])
410 }
411
412 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
413 let prefix = config.node_type_prefix();
414 let content = gen_dts(api, &prefix);
415
416 let src_dir = resolve_output_dir(
421 config.output.node.as_ref(),
422 &config.crate_config.name,
423 "crates/{name}-node/src/",
424 );
425 let crate_root = {
426 let p = PathBuf::from(&src_dir);
427 match p.file_name().and_then(|n| n.to_str()) {
428 Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
429 _ => p,
430 }
431 };
432
433 Ok(vec![GeneratedFile {
434 path: crate_root.join("index.d.ts"),
435 content,
436 generated_header: false,
437 }])
438 }
439
440 fn build_config(&self) -> Option<BuildConfig> {
441 Some(BuildConfig {
442 tool: "napi",
443 crate_suffix: "-node",
444 build_dep: BuildDependency::None,
445 post_build: vec![PostBuildStep::PatchFile {
446 path: "index.d.ts",
447 find: "export declare const enum",
448 replace: "export declare enum",
449 }],
450 })
451 }
452}
453
454fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
456 let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
457 struct_builder.add_attr("napi(object)");
459 struct_builder.add_derive("Clone");
460 struct_builder.add_derive("Default");
464 if has_serde {
468 struct_builder.add_derive("serde::Serialize");
469 struct_builder.add_derive("serde::Deserialize");
470 }
471
472 for field in &typ.fields {
473 let mapped_type = mapper.map_type(&field.ty);
474 let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
478 format!("Option<{}>", mapped_type)
479 } else {
480 mapped_type
481 };
482 let js_name = to_node_name(&field.name);
483 let attrs = if js_name != field.name {
484 vec![format!("napi(js_name = \"{}\")", js_name)]
485 } else {
486 vec![]
487 };
488 struct_builder.add_field(&field.name, &field_type, attrs);
489 }
490
491 struct_builder.build()
492}
493
494fn gen_opaque_struct_methods(
496 typ: &TypeDef,
497 mapper: &NapiMapper,
498 cfg: &RustBindingConfig,
499 opaque_types: &AHashSet<String>,
500 prefix: &str,
501 adapter_bodies: &alef_adapters::AdapterBodies,
502) -> String {
503 let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
504 impl_builder.add_attr("napi");
505
506 let (instance, statics) = partition_methods(&typ.methods);
507
508 for method in &instance {
509 let adapter_key = format!("{}.{}", typ.name, method.name);
512 if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
513 continue;
514 }
515 impl_builder.add_method(&gen_opaque_instance_method(
516 method,
517 mapper,
518 typ,
519 cfg,
520 opaque_types,
521 prefix,
522 adapter_bodies,
523 ));
524 }
525 for method in &statics {
526 let adapter_key = format!("{}.{}", typ.name, method.name);
528 if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
529 continue;
530 }
531 impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
532 }
533
534 impl_builder.build()
535}
536
537fn gen_opaque_instance_method(
539 method: &MethodDef,
540 mapper: &NapiMapper,
541 typ: &TypeDef,
542 cfg: &RustBindingConfig,
543 opaque_types: &AHashSet<String>,
544 prefix: &str,
545 adapter_bodies: &alef_adapters::AdapterBodies,
546) -> String {
547 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
548 let return_type = mapper.map_type(&method.return_type);
549 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
550
551 let js_name = to_node_name(&method.name);
552 let js_name_attr = if js_name != method.name {
553 format!("(js_name = \"{}\")", js_name)
554 } else {
555 String::new()
556 };
557
558 let async_kw = if method.is_async { "async " } else { "" };
559
560 let type_name = &typ.name;
561 let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
562 let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
563 let call_args = napi_gen_call_args(&method.params, opaque_types);
564
565 let opaque_can_delegate = !method.sanitized
568 && !is_ref_mut_receiver
569 && (!is_owned_receiver || typ.is_clone)
570 && method
571 .params
572 .iter()
573 .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
574 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
575
576 let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
577
578 let async_result_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
588 let adapter_key = format!("{type_name}.{}", method.name);
589 let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
590 adapter_body.clone()
591 } else if !opaque_can_delegate {
592 if cfg.has_serde
594 && !method.sanitized
595 && generators::has_named_params(&method.params, opaque_types)
596 && method.error_type.is_some()
597 && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
598 {
599 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
600 let serde_bindings =
601 generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
602 let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
603 let core_call = format!("self.inner.{}({serde_call_args})", method.name);
604 if matches!(method.return_type, TypeRef::Unit) {
605 format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
606 } else {
607 let wrap = napi_wrap_return(
608 "result",
609 &method.return_type,
610 type_name,
611 opaque_types,
612 true,
613 method.returns_ref,
614 prefix,
615 );
616 format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
617 }
618 } else {
619 generators::gen_unimplemented_body(
620 &method.return_type,
621 &format!("{type_name}.{}", method.name),
622 method.error_type.is_some(),
623 cfg,
624 &method.params,
625 opaque_types,
626 )
627 }
628 } else if method.is_async {
629 let inner_clone_line = "let inner = self.inner.clone();\n ";
630 let core_call_str = make_async_core_call(&method.name);
631 generators::gen_async_body(
632 &core_call_str,
633 cfg,
634 method.error_type.is_some(),
635 &async_result_wrap,
636 true,
637 inner_clone_line,
638 matches!(method.return_type, TypeRef::Unit),
639 Some(&return_type),
640 )
641 } else {
642 let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
646 let (let_bindings, call_args_for_call) = if use_let_bindings {
647 let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
648 let args = napi_apply_primitive_casts_to_call_args(
649 &generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
650 &method.params,
651 );
652 (bindings, args)
653 } else {
654 (String::new(), napi_gen_call_args(&method.params, opaque_types))
655 };
656 let core_call = if is_owned_receiver {
657 format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
658 } else {
659 format!("self.inner.{}({})", method.name, call_args_for_call)
660 };
661 if method.error_type.is_some() {
662 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
663 if matches!(method.return_type, TypeRef::Unit) {
664 format!("{let_bindings}{core_call}{err_conv}?;\n Ok(())")
665 } else {
666 let wrap = napi_wrap_return(
667 "result",
668 &method.return_type,
669 type_name,
670 opaque_types,
671 true,
672 method.returns_ref,
673 prefix,
674 );
675 format!("{let_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
676 }
677 } else {
678 format!(
679 "{let_bindings}{}",
680 napi_wrap_return(
681 &core_call,
682 &method.return_type,
683 type_name,
684 opaque_types,
685 true,
686 method.returns_ref,
687 prefix,
688 )
689 )
690 }
691 };
692
693 let mut attrs = String::new();
694 if method.params.len() + 1 > 7 {
696 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
697 }
698 if method.error_type.is_some() {
700 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
701 }
702 if generators::is_trait_method_name(&method.name) {
704 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
705 }
706 format!(
707 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
708 {body}\n}}",
709 method.name
710 )
711}
712
713fn gen_static_method(
715 method: &MethodDef,
716 mapper: &NapiMapper,
717 typ: &TypeDef,
718 cfg: &RustBindingConfig,
719 opaque_types: &AHashSet<String>,
720 prefix: &str,
721) -> String {
722 let params = function_params(&method.params, &|ty| mapper.map_type(ty));
723 let return_type = mapper.map_type(&method.return_type);
724 let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
725
726 let js_name = to_node_name(&method.name);
727 let js_name_attr = if js_name != method.name {
728 format!("(js_name = \"{}\")", js_name)
729 } else {
730 String::new()
731 };
732
733 let type_name = &typ.name;
734 let core_type_path = typ.rust_path.replace('-', "_");
735 let call_args = napi_gen_call_args(&method.params, opaque_types);
736 let can_delegate_static = can_auto_delegate(method, opaque_types);
737
738 let async_kw = if method.is_async { "async " } else { "" };
739
740 let body = if !can_delegate_static {
741 generators::gen_unimplemented_body(
742 &method.return_type,
743 &format!("{type_name}::{}", method.name),
744 method.error_type.is_some(),
745 cfg,
746 &method.params,
747 opaque_types,
748 )
749 } else if method.is_async {
750 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
751 let return_wrap = napi_wrap_return(
752 "result",
753 &method.return_type,
754 type_name,
755 opaque_types,
756 typ.is_opaque,
757 method.returns_ref,
758 prefix,
759 );
760 generators::gen_async_body(
761 &core_call,
762 cfg,
763 method.error_type.is_some(),
764 &return_wrap,
765 false,
766 "",
767 matches!(method.return_type, TypeRef::Unit),
768 Some(&return_type),
769 )
770 } else {
771 let core_call = format!("{core_type_path}::{}({call_args})", method.name);
772 if method.error_type.is_some() {
773 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
774 let wrapped = napi_wrap_return(
775 "val",
776 &method.return_type,
777 type_name,
778 opaque_types,
779 typ.is_opaque,
780 method.returns_ref,
781 prefix,
782 );
783 if wrapped == "val" {
784 format!("{core_call}{err_conv}")
785 } else {
786 format!("{core_call}.map(|val| {wrapped}){err_conv}")
787 }
788 } else {
789 napi_wrap_return(
790 &core_call,
791 &method.return_type,
792 type_name,
793 opaque_types,
794 typ.is_opaque,
795 method.returns_ref,
796 prefix,
797 )
798 }
799 };
800
801 let mut attrs = String::new();
802 if method.params.len() > 7 {
804 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
805 }
806 if method.error_type.is_some() {
808 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
809 }
810 if generators::is_trait_method_name(&method.name) {
812 attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
813 }
814 format!(
815 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
816 {body}\n}}",
817 method.name
818 )
819}
820
821fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
827 let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
828
829 if is_tagged_data_enum {
830 return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
831 }
832
833 let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
835 "snake_case" => Some("snake_case"),
836 "camelCase" => Some("camelCase"),
837 "kebab-case" => Some("kebab-case"),
838 "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
839 "lowercase" => Some("lowercase"),
840 "UPPERCASE" => Some("UPPERCASE"),
841 "PascalCase" => Some("PascalCase"),
842 _ => None,
843 });
844
845 let string_enum_attr = match napi_case {
846 Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
847 None => "#[napi(string_enum)]".to_string(),
848 };
849
850 let derives = if has_serde {
851 "#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
852 } else {
853 "#[derive(Clone)]".to_string()
854 };
855 let mut lines = vec![
856 string_enum_attr,
857 derives,
858 format!("pub enum {prefix}{} {{", enum_def.name),
859 ];
860
861 for variant in &enum_def.variants {
862 lines.push(format!(" {},", variant.name));
863 }
864
865 lines.push("}".to_string());
866
867 if let Some(first) = enum_def.variants.first() {
869 lines.push(String::new());
870 lines.push("#[allow(clippy::derivable_impls)]".to_string());
871 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
872 lines.push(format!(" fn default() -> Self {{ Self::{} }}", first.name));
873 lines.push("}".to_string());
874 }
875
876 lines.join("\n")
877}
878
879fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
892 use alef_codegen::type_mapper::TypeMapper;
893 let mapper = NapiMapper::new(prefix.to_string());
894
895 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
896
897 let derive = if has_serde {
898 "#[derive(Clone, serde::Serialize, serde::Deserialize)]"
899 } else {
900 "#[derive(Clone)]"
901 };
902 let mut lines = vec![
903 derive.to_string(),
904 "#[napi(object)]".to_string(),
905 format!("pub struct {prefix}{} {{", enum_def.name),
906 format!(" #[napi(js_name = \"{tag_field}\")]"),
907 format!(" pub {tag_field}_tag: String,"),
908 ];
909
910 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
914
915 let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
917 for variant in &enum_def.variants {
918 for field in &variant.fields {
919 if seen_fields.insert(field.name.clone()) {
920 let field_type = if (field.sanitized || mixed_named_fields.contains(&field.name))
923 && matches!(&field.ty, TypeRef::Named(_))
924 {
925 "String".to_string()
926 } else {
927 mapper.map_type(&field.ty).to_string()
928 };
929 let js_name = alef_codegen::naming::to_node_name(&field.name);
930 if js_name != field.name {
931 lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
932 }
933 lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
934 }
935 }
936 }
937
938 lines.push("}".to_string());
939
940 lines.push(String::new());
942 lines.push("#[allow(clippy::derivable_impls)]".to_string());
943 lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
944 lines.push(format!(
945 " fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
946 seen_fields
947 .iter()
948 .map(|f| format!("{f}: None"))
949 .collect::<Vec<_>>()
950 .join(", ")
951 ));
952 lines.push("}".to_string());
953
954 lines.join("\n")
955}
956
957fn gen_function(
959 func: &FunctionDef,
960 mapper: &NapiMapper,
961 cfg: &RustBindingConfig,
962 opaque_types: &AHashSet<String>,
963 prefix: &str,
964) -> String {
965 let params = function_params(&func.params, &|ty| {
966 if let TypeRef::Named(n) = ty {
969 if opaque_types.contains(n.as_str()) {
970 return format!("&{prefix}{n}");
971 }
972 }
973 mapper.map_type(ty)
974 });
975 let return_type = mapper.map_type(&func.return_type);
976 let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
977
978 let js_name = to_node_name(&func.name);
979 let js_name_attr = if js_name != func.name {
980 format!("(js_name = \"{}\")", js_name)
981 } else {
982 String::new()
983 };
984
985 let core_import = cfg.core_import;
986 let core_fn_path = {
987 let path = func.rust_path.replace('-', "_");
988 if path.starts_with(core_import) {
989 path
990 } else {
991 format!("{core_import}::{}", func.name)
992 }
993 };
994
995 let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
997 || func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
998 let call_args = if use_let_bindings {
999 let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
1000 napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
1001 } else {
1002 napi_gen_call_args(&func.params, opaque_types)
1003 };
1004
1005 let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
1006
1007 let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
1008
1009 let async_kw = if func.is_async { "async " } else { "" };
1010
1011 let body = if !can_delegate_fn {
1012 if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
1015 let serde_bindings =
1016 generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
1017 let vec_str_bindings: String = func.params.iter().filter(|p| {
1019 p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
1020 }).map(|p| {
1021 format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n ", p.name, p.name)
1022 }).collect();
1023 let core_call = format!("{core_fn_path}({call_args})");
1024 let await_kw = if func.is_async { ".await" } else { "" };
1025
1026 if matches!(func.return_type, TypeRef::Unit) {
1027 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n Ok(())")
1028 } else {
1029 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1030 if wrapped == "val" {
1031 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
1032 } else {
1033 format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
1034 }
1035 }
1036 } else {
1037 generators::gen_unimplemented_body(
1038 &func.return_type,
1039 &func.name,
1040 func.error_type.is_some(),
1041 cfg,
1042 &func.params,
1043 opaque_types,
1044 )
1045 }
1046 } else if func.is_async {
1047 let mut let_bindings = if use_let_bindings {
1049 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1050 } else {
1051 String::new()
1052 };
1053 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1055 let core_call = format!("{core_fn_path}({call_args})");
1056 let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
1057 let return_type = mapper.map_type(&func.return_type);
1058 generators::gen_async_body(
1059 &core_call,
1060 cfg,
1061 func.error_type.is_some(),
1062 &return_wrap,
1063 false,
1064 &let_bindings,
1065 matches!(func.return_type, TypeRef::Unit),
1066 Some(&return_type),
1067 )
1068 } else {
1069 let core_call = format!("{core_fn_path}({call_args})");
1070 let mut let_bindings = if use_let_bindings {
1072 generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1073 } else {
1074 String::new()
1075 };
1076 let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1078
1079 if func.error_type.is_some() {
1080 let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1081 if wrapped == "val" {
1082 format!("{let_bindings}{core_call}{err_conv}")
1083 } else {
1084 format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
1085 }
1086 } else {
1087 format!(
1088 "{let_bindings}{}",
1089 napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
1090 )
1091 }
1092 };
1093
1094 let mut attrs = String::new();
1095 if func.params.len() > 7 {
1097 attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
1098 }
1099 if func.error_type.is_some() {
1101 attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
1102 }
1103 format!(
1104 "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
1105 {body}\n}}",
1106 func.name
1107 )
1108}
1109
1110fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
1113 let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
1115 args_list
1116 .iter()
1117 .zip(params.iter())
1118 .map(|(arg, p)| {
1119 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1121 return format!("&{}_f32", p.name);
1122 }
1123 match &p.ty {
1124 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1125 let core_ty = core_prim_str(prim);
1126 if p.optional {
1127 if arg.contains(".map(") || arg.contains(".as_") {
1129 arg.to_string()
1131 } else {
1132 format!("{}.map(|v| v as {})", arg, core_ty)
1133 }
1134 } else {
1135 format!("{} as {}", arg, core_ty)
1137 }
1138 }
1139 _ => arg.to_string(),
1140 }
1141 })
1142 .collect::<Vec<_>>()
1143 .join(", ")
1144}
1145
1146fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
1149 let mut bindings = String::new();
1150 for p in params {
1151 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1152 let conv_name = format!("{}_f32", p.name);
1153 bindings.push_str(&format!(
1154 " let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
1155 p.name
1156 ));
1157 }
1158 }
1159 bindings
1160}
1161
1162fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
1165 params
1166 .iter()
1167 .map(|p| {
1168 if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1170 return format!("&{}_f32", p.name);
1171 }
1172 match &p.ty {
1173 TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1174 let core_ty = core_prim_str(prim);
1175 if p.optional {
1176 format!("{}.map(|v| v as {})", p.name, core_ty)
1177 } else {
1178 format!("{} as {}", p.name, core_ty)
1179 }
1180 }
1181 TypeRef::Duration => {
1182 if p.optional {
1183 format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
1184 } else {
1185 format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
1186 }
1187 }
1188 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1189 if p.optional {
1190 format!("{}.as_ref().map(|v| &v.inner)", p.name)
1191 } else {
1192 format!("&{}.inner", p.name)
1193 }
1194 }
1195 TypeRef::Named(_) => {
1196 if p.optional {
1197 if p.is_ref {
1198 format!("{}.as_ref()", p.name)
1199 } else {
1200 format!("{}.map(Into::into)", p.name)
1201 }
1202 } else {
1203 format!("{}.into()", p.name)
1204 }
1205 }
1206 TypeRef::String | TypeRef::Char => {
1207 if p.optional {
1208 if p.is_ref {
1209 format!("{}.as_deref()", p.name)
1210 } else {
1211 p.name.clone()
1212 }
1213 } else if p.is_ref {
1214 format!("&{}", p.name)
1215 } else {
1216 p.name.clone()
1217 }
1218 }
1219 TypeRef::Path => {
1220 if p.optional {
1221 if p.is_ref {
1222 format!("{}.as_deref().map(std::path::Path::new)", p.name)
1223 } else {
1224 format!("{}.map(std::path::PathBuf::from)", p.name)
1225 }
1226 } else if p.is_ref {
1227 format!("std::path::Path::new(&{})", p.name)
1228 } else {
1229 format!("std::path::PathBuf::from({})", p.name)
1230 }
1231 }
1232 TypeRef::Bytes => {
1233 if p.optional {
1234 if p.is_ref {
1235 format!("{}.as_deref()", p.name)
1236 } else {
1237 p.name.clone()
1238 }
1239 } else if p.is_ref {
1240 format!("&{}", p.name)
1241 } else {
1242 p.name.clone()
1243 }
1244 }
1245 TypeRef::Vec(inner) => {
1246 if p.optional {
1247 if p.is_ref {
1248 format!("{}.as_deref()", p.name)
1249 } else {
1250 p.name.clone()
1251 }
1252 } else if p.is_ref && matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) {
1253 format!("&{}_refs", p.name)
1254 } else if p.is_ref {
1255 format!("&{}", p.name)
1256 } else {
1257 p.name.clone()
1258 }
1259 }
1260 TypeRef::Map(_, _) => {
1261 if p.optional {
1262 if p.is_ref {
1263 format!("{}.as_ref()", p.name)
1264 } else {
1265 p.name.clone()
1266 }
1267 } else if p.is_ref {
1268 format!("&{}", p.name)
1269 } else {
1270 p.name.clone()
1271 }
1272 }
1273 _ => p.name.clone(),
1274 }
1275 })
1276 .collect::<Vec<_>>()
1277 .join(", ")
1278}
1279
1280fn napi_wrap_return(
1283 expr: &str,
1284 return_type: &TypeRef,
1285 type_name: &str,
1286 opaque_types: &AHashSet<String>,
1287 self_is_opaque: bool,
1288 returns_ref: bool,
1289 prefix: &str,
1290) -> String {
1291 match return_type {
1292 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1293 format!("{expr} as i64")
1294 }
1295 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1296 TypeRef::Named(n) if n == type_name && self_is_opaque => {
1298 if returns_ref {
1299 format!("Self {{ inner: Arc::new({expr}.clone()) }}")
1300 } else {
1301 format!("Self {{ inner: Arc::new({expr}) }}")
1302 }
1303 }
1304 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1305 if returns_ref {
1306 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1307 } else {
1308 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1309 }
1310 }
1311 TypeRef::Named(_) => {
1312 if returns_ref {
1313 format!("{expr}.clone().into()")
1314 } else {
1315 format!("{expr}.into()")
1316 }
1317 }
1318 _ => generators::wrap_return(
1319 expr,
1320 return_type,
1321 type_name,
1322 opaque_types,
1323 self_is_opaque,
1324 returns_ref,
1325 false,
1326 ),
1327 }
1328}
1329
1330fn napi_wrap_return_fn(
1332 expr: &str,
1333 return_type: &TypeRef,
1334 opaque_types: &AHashSet<String>,
1335 returns_ref: bool,
1336 prefix: &str,
1337) -> String {
1338 match return_type {
1339 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1340 format!("{expr} as i64")
1341 }
1342 TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1343 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1344 if returns_ref {
1345 format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1346 } else {
1347 format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1348 }
1349 }
1350 TypeRef::Named(_) => {
1351 if returns_ref {
1352 format!("{expr}.clone().into()")
1353 } else {
1354 format!("{expr}.into()")
1355 }
1356 }
1357 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1358 if returns_ref {
1359 format!("{expr}.into()")
1360 } else {
1361 expr.to_string()
1362 }
1363 }
1364 TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1365 TypeRef::Json => format!("{expr}.to_string()"),
1366 TypeRef::Optional(inner) => match inner.as_ref() {
1367 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1368 if returns_ref {
1369 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
1370 } else {
1371 format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
1372 }
1373 }
1374 TypeRef::Named(_) => {
1375 if returns_ref {
1376 format!("{expr}.map(|v| v.clone().into())")
1377 } else {
1378 format!("{expr}.map(Into::into)")
1379 }
1380 }
1381 TypeRef::Vec(inner) => match inner.as_ref() {
1382 TypeRef::Named(_) => {
1383 if returns_ref {
1384 format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
1385 } else {
1386 format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
1387 }
1388 }
1389 _ => expr.to_string(),
1390 },
1391 TypeRef::Path => {
1392 format!("{expr}.map(Into::into)")
1393 }
1394 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1395 if returns_ref {
1396 format!("{expr}.map(Into::into)")
1397 } else {
1398 expr.to_string()
1399 }
1400 }
1401 _ => expr.to_string(),
1402 },
1403 TypeRef::Vec(inner) => match inner.as_ref() {
1404 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1405 let target_ty = match p {
1407 alef_core::ir::PrimitiveType::F32 => "f64",
1408 _ => "i64", };
1410 format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
1411 }
1412 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1413 if returns_ref {
1414 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1415 } else {
1416 format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
1417 }
1418 }
1419 TypeRef::Named(_) => {
1420 if returns_ref {
1421 format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1422 } else {
1423 format!("{expr}.into_iter().map(Into::into).collect()")
1424 }
1425 }
1426 TypeRef::Path => {
1427 format!("{expr}.into_iter().map(Into::into).collect()")
1428 }
1429 TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1430 if returns_ref {
1431 format!("{expr}.into_iter().map(Into::into).collect()")
1432 } else {
1433 expr.to_string()
1434 }
1435 }
1436 _ => expr.to_string(),
1437 },
1438 _ => expr.to_string(),
1439 }
1440}
1441
1442fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
1444 matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
1445}
1446
1447fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1448 matches!(
1452 p,
1453 alef_core::ir::PrimitiveType::U64
1454 | alef_core::ir::PrimitiveType::Usize
1455 | alef_core::ir::PrimitiveType::Isize
1456 | alef_core::ir::PrimitiveType::F32
1457 )
1458}
1459
1460fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1461 match p {
1462 alef_core::ir::PrimitiveType::U64 => "u64",
1463 alef_core::ir::PrimitiveType::Usize => "usize",
1464 alef_core::ir::PrimitiveType::Isize => "isize",
1465 alef_core::ir::PrimitiveType::F32 => "f32",
1466 _ => unreachable!(),
1467 }
1468}
1469
1470fn gen_tokio_runtime() -> String {
1472 "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1473 tokio::runtime::Builder::new_multi_thread()
1474 .enable_all()
1475 .build()
1476 .expect(\"Failed to create Tokio runtime\")
1477});"
1478 .to_string()
1479}
1480
1481fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
1491 let header = hash::header(CommentStyle::DoubleSlash);
1492 let mut lines: Vec<String> = header.lines().map(|l| l.to_string()).collect();
1493 lines.push("/* eslint-disable */".to_string());
1494
1495 let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1500 opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1501
1502 let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1504 plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1505
1506 let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1508 sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1509
1510 let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1512 sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1513
1514 enum Decl<'a> {
1517 Class(&'a TypeDef),
1518 Interface(&'a TypeDef),
1519 Enum(&'a EnumDef),
1520 Function(&'a FunctionDef),
1521 }
1522
1523 let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1524 for t in &opaque_types {
1525 all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
1526 }
1527 for t in &plain_types {
1528 all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
1529 }
1530 for e in &sorted_enums {
1531 all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
1532 }
1533 for f in &sorted_fns {
1534 all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1535 }
1536 all_decls.sort_by_key(|a| a.0.to_lowercase());
1537
1538 for (_, decl) in &all_decls {
1539 lines.push(String::new());
1540 match decl {
1541 Decl::Class(typ) => {
1542 lines.extend(format_jsdoc(&typ.doc, ""));
1543 lines.push(format!("export declare class {prefix}{} {{", typ.name));
1544 for method in &typ.methods {
1545 let js_name = to_node_name(&method.name);
1546 let params = dts_params(&method.params, prefix);
1547 let ret = dts_return_type(
1548 &method.return_type,
1549 method.error_type.is_some(),
1550 method.is_async,
1551 prefix,
1552 );
1553 lines.extend(format_jsdoc(&method.doc, " "));
1554 if method.is_static {
1555 lines.push(format!(" static {js_name}({params}): {ret}"));
1556 } else {
1557 lines.push(format!(" {js_name}({params}): {ret}"));
1558 }
1559 }
1560 lines.push("}".to_string());
1561 }
1562 Decl::Interface(typ) => {
1563 lines.extend(format_jsdoc(&typ.doc, ""));
1564 lines.push(format!("export interface {prefix}{} {{", typ.name));
1565 for field in &typ.fields {
1566 let js_name = to_node_name(&field.name);
1567 let ts_ty = dts_type(&field.ty, prefix);
1568 lines.extend(format_jsdoc(&field.doc, " "));
1569 if matches!(field.ty, TypeRef::Optional(_)) {
1572 lines.push(format!(" {js_name}?: {ts_ty}"));
1573 } else {
1574 lines.push(format!(" {js_name}: {ts_ty}"));
1575 }
1576 }
1577 lines.push("}".to_string());
1578 }
1579 Decl::Enum(e) => {
1580 let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
1581 lines.extend(format_jsdoc(&e.doc, ""));
1582 if is_data_enum {
1583 let tag_field = e.serde_tag.as_deref().unwrap_or("type");
1586 let mut member_lines: Vec<String> = Vec::new();
1587 for variant in &e.variants {
1588 let tag_value = variant
1589 .serde_rename
1590 .as_deref()
1591 .map(|s| s.to_string())
1592 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1593 let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
1594 for field in &variant.fields {
1595 let js_name = to_node_name(&field.name);
1596 let ts_ty = dts_type(&field.ty, prefix);
1597 if matches!(field.ty, TypeRef::Optional(_)) {
1598 obj_fields.push(format!("{js_name}?: {ts_ty}"));
1599 } else {
1600 obj_fields.push(format!("{js_name}: {ts_ty}"));
1601 }
1602 }
1603 member_lines.push(format!(" | {{ {} }}", obj_fields.join("; ")));
1604 }
1605 lines.push(format!("export type {prefix}{} =", e.name));
1606 lines.extend(member_lines);
1607 } else {
1608 lines.push(format!("export declare enum {prefix}{} {{", e.name));
1609 for variant in &e.variants {
1610 let value = variant
1613 .serde_rename
1614 .as_deref()
1615 .map(|s| s.to_string())
1616 .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1617 lines.extend(format_jsdoc(&variant.doc, " "));
1618 lines.push(format!(" {} = \"{}\",", variant.name, value));
1619 }
1620 lines.push("}".to_string());
1621 }
1622 }
1623 Decl::Function(func) => {
1624 let js_name = to_node_name(&func.name);
1625 let params = dts_params(&func.params, prefix);
1626 let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
1627 lines.extend(format_jsdoc(&func.doc, ""));
1628 lines.push(format!("export declare function {js_name}({params}): {ret};"));
1629 }
1630 }
1631 }
1632
1633 lines.push(String::new());
1634 lines.join("\n")
1635}
1636
1637fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
1643 let doc = doc.trim();
1644 if doc.is_empty() {
1645 return vec![];
1646 }
1647 let lines: Vec<&str> = doc.lines().collect();
1648 if lines.len() == 1 {
1649 vec![format!("{indent}/** {} */", lines[0].trim())]
1650 } else {
1651 let mut out = Vec::with_capacity(lines.len() + 2);
1652 out.push(format!("{indent}/**"));
1653 for line in &lines {
1654 let trimmed = line.trim();
1655 if trimmed.is_empty() {
1656 out.push(format!("{indent} *"));
1657 } else {
1658 out.push(format!("{indent} * {trimmed}"));
1659 }
1660 }
1661 out.push(format!("{indent} */"));
1662 out
1663 }
1664}
1665
1666fn dts_type(ty: &TypeRef, prefix: &str) -> String {
1668 match ty {
1669 TypeRef::Primitive(p) => match p {
1670 alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1671 alef_core::ir::PrimitiveType::U8
1672 | alef_core::ir::PrimitiveType::U16
1673 | alef_core::ir::PrimitiveType::U32
1674 | alef_core::ir::PrimitiveType::I8
1675 | alef_core::ir::PrimitiveType::I16
1676 | alef_core::ir::PrimitiveType::I32
1677 | alef_core::ir::PrimitiveType::F32
1678 | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1679 alef_core::ir::PrimitiveType::U64
1681 | alef_core::ir::PrimitiveType::I64
1682 | alef_core::ir::PrimitiveType::Usize
1683 | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1684 },
1685 TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1686 TypeRef::Bytes => "Uint8Array".to_string(),
1687 TypeRef::Json => "unknown".to_string(),
1688 TypeRef::Duration => "number".to_string(),
1689 TypeRef::Unit => "void".to_string(),
1690 TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
1691 TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
1692 TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
1693 TypeRef::Named(name) => format!("{prefix}{name}"),
1694 }
1695}
1696
1697fn dts_params(params: &[ParamDef], prefix: &str) -> String {
1699 let mut required: Vec<&ParamDef> = Vec::new();
1704 let mut optional: Vec<&ParamDef> = Vec::new();
1705 for p in params {
1706 if p.optional {
1707 optional.push(p);
1708 } else {
1709 required.push(p);
1710 }
1711 }
1712 let ordered: Vec<&ParamDef> = if params
1714 .iter()
1715 .zip(required.iter().chain(optional.iter()))
1716 .all(|(a, b)| std::ptr::eq(a as *const ParamDef, *b as *const ParamDef))
1717 {
1718 params.iter().collect()
1719 } else {
1720 required.into_iter().chain(optional).collect()
1721 };
1722 ordered
1723 .iter()
1724 .map(|p| {
1725 let js_name = to_node_name(&p.name);
1726 let ts_ty = dts_type(&p.ty, prefix);
1727 if p.optional {
1728 format!("{js_name}?: {ts_ty} | undefined | null")
1729 } else {
1730 format!("{js_name}: {ts_ty}")
1731 }
1732 })
1733 .collect::<Vec<_>>()
1734 .join(", ")
1735}
1736
1737fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
1742 let base = match ret {
1743 TypeRef::Unit => "void".to_string(),
1744 other => dts_type(other, prefix),
1745 };
1746 if is_async { format!("Promise<{base}>") } else { base }
1747}
1748
1749fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
1754 match rename_all {
1755 Some("snake_case") => {
1756 let mut out = String::with_capacity(variant_name.len() + 4);
1758 for (i, c) in variant_name.chars().enumerate() {
1759 if c.is_uppercase() && i > 0 {
1760 out.push('_');
1761 }
1762 out.extend(c.to_lowercase());
1763 }
1764 out
1765 }
1766 Some("camelCase") => {
1767 let mut chars = variant_name.chars();
1769 match chars.next() {
1770 None => String::new(),
1771 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1772 }
1773 }
1774 Some("kebab-case") => {
1775 let mut out = String::with_capacity(variant_name.len() + 4);
1776 for (i, c) in variant_name.chars().enumerate() {
1777 if c.is_uppercase() && i > 0 {
1778 out.push('-');
1779 }
1780 out.extend(c.to_lowercase());
1781 }
1782 out
1783 }
1784 Some("SCREAMING_SNAKE_CASE") => {
1785 let mut out = String::with_capacity(variant_name.len() + 4);
1786 for (i, c) in variant_name.chars().enumerate() {
1787 if c.is_uppercase() && i > 0 {
1788 out.push('_');
1789 }
1790 out.extend(c.to_uppercase());
1791 }
1792 out
1793 }
1794 Some("lowercase") => variant_name.to_lowercase(),
1795 Some("UPPERCASE") => variant_name.to_uppercase(),
1796 _ => variant_name.to_string(),
1798 }
1799}
1800
1801fn gen_tagged_enum_binding_to_core(
1803 enum_def: &EnumDef,
1804 core_import: &str,
1805 prefix: &str,
1806 struct_names: &ahash::AHashSet<String>,
1807) -> String {
1808 use alef_core::ir::TypeRef;
1809 use std::fmt::Write;
1810 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1811 let binding_name = format!("{prefix}{}", enum_def.name);
1812 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1813
1814 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1819 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1822
1823 let mut out = String::with_capacity(512);
1824 writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1825 writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
1826 writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
1827
1828 for variant in &enum_def.variants {
1829 let default_tag = variant.name.to_lowercase();
1830 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1831 if variant.fields.is_empty() {
1832 writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
1833 } else {
1834 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1835 let field_exprs: Vec<String> = variant
1836 .fields
1837 .iter()
1838 .map(|f| {
1839 let has_binding = fields_with_binding_struct.contains(f.name.as_str());
1840 let is_mixed = mixed_named_fields.contains(&f.name);
1841 if f.optional {
1842 match &f.ty {
1843 TypeRef::Path => {
1844 format!("val.{}.map(std::path::PathBuf::from)", f.name)
1845 }
1846 TypeRef::Named(n) if is_mixed => {
1847 let core_type = format!("{core_import}::{n}");
1849 format!(
1850 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
1851 f.name
1852 )
1853 }
1854 TypeRef::Named(_) if has_binding => {
1855 format!("val.{}.map(|v| v.into())", f.name)
1856 }
1857 TypeRef::Named(_) => {
1860 format!("val.{}.map(|v| v.into())", f.name)
1861 }
1862 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1863 let core_ty = core_prim_str(p);
1864 format!("val.{}.map(|v| v as {core_ty})", f.name)
1865 }
1866 _ => {
1867 format!("val.{}", f.name)
1868 }
1869 }
1870 } else if f.sanitized {
1871 let expr = "Default::default()".to_string();
1872 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1873 } else {
1874 let expr = match &f.ty {
1875 TypeRef::Named(n) if is_mixed => {
1876 let core_type = format!("{core_import}::{n}");
1878 format!(
1879 "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
1880 f.name
1881 )
1882 }
1883 TypeRef::Named(_) if has_binding => {
1884 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1885 }
1886 TypeRef::Named(_) => {
1889 format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1890 }
1891 TypeRef::Path => {
1892 format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
1893 }
1894 TypeRef::Primitive(p) if needs_napi_cast(p) => {
1895 let core_ty = core_prim_str(p);
1896 format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
1897 }
1898 _ => {
1899 format!("val.{}.unwrap_or_default()", f.name)
1900 }
1901 };
1902 if f.is_boxed { format!("Box::new({expr})") } else { expr }
1903 }
1904 })
1905 .collect();
1906 if is_tuple {
1907 writeln!(
1908 out,
1909 " \"{tag_value}\" => Self::{}({}),",
1910 variant.name,
1911 field_exprs.join(", ")
1912 )
1913 .ok();
1914 } else {
1915 let field_inits: Vec<String> = variant
1916 .fields
1917 .iter()
1918 .zip(field_exprs.iter())
1919 .map(|(f, expr)| format!("{}: {expr}", f.name))
1920 .collect();
1921 writeln!(
1922 out,
1923 " \"{tag_value}\" => Self::{} {{ {} }},",
1924 variant.name,
1925 field_inits.join(", ")
1926 )
1927 .ok();
1928 }
1929 }
1930 }
1931
1932 if let Some(first) = enum_def.variants.first() {
1934 if first.fields.is_empty() {
1935 writeln!(out, " _ => Self::{},", first.name).ok();
1936 } else {
1937 let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1938 if is_tuple {
1939 let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1940 writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1941 } else {
1942 let defaults: Vec<String> = first
1943 .fields
1944 .iter()
1945 .map(|f| format!("{}: Default::default()", f.name))
1946 .collect();
1947 writeln!(
1948 out,
1949 " _ => Self::{} {{ {} }},",
1950 first.name,
1951 defaults.join(", ")
1952 )
1953 .ok();
1954 }
1955 }
1956 }
1957
1958 writeln!(out, " }}").ok();
1959 writeln!(out, " }}").ok();
1960 write!(out, "}}").ok();
1961 out
1962}
1963
1964fn gen_tagged_enum_core_to_binding(
1966 enum_def: &EnumDef,
1967 core_import: &str,
1968 prefix: &str,
1969 struct_names: &ahash::AHashSet<String>,
1970) -> String {
1971 use std::fmt::Write;
1972 let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1973 let binding_name = format!("{prefix}{}", enum_def.name);
1974 let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1975 let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1976 let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1979
1980 let all_fields: Vec<String> = {
1982 let mut fields = std::collections::BTreeSet::new();
1983 for v in &enum_def.variants {
1984 for f in &v.fields {
1985 fields.insert(f.name.clone());
1986 }
1987 }
1988 fields.into_iter().collect()
1989 };
1990
1991 let mut out = String::with_capacity(512);
1992 writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1993 writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
1994 writeln!(out, " match val {{").ok();
1995
1996 for variant in &enum_def.variants {
1997 let default_tag = variant.name.to_lowercase();
1998 let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1999 let _variant_field_names: std::collections::BTreeSet<String> =
2000 variant.fields.iter().map(|f| f.name.clone()).collect();
2001
2002 if variant.fields.is_empty() {
2003 writeln!(
2004 out,
2005 " {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2006 variant.name,
2007 all_fields
2008 .iter()
2009 .map(|f| format!("{f}: None"))
2010 .collect::<Vec<_>>()
2011 .join(", ")
2012 )
2013 .ok();
2014 } else {
2015 use alef_core::ir::TypeRef;
2016 let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
2017 let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
2018 variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
2019 let destructured: Vec<String> = variant
2020 .fields
2021 .iter()
2022 .map(|f| {
2023 if f.sanitized {
2024 if is_tuple {
2025 format!("_{}", f.name)
2026 } else {
2027 format!("{}: _{}", f.name, f.name)
2028 }
2029 } else {
2030 f.name.clone()
2031 }
2032 })
2033 .collect();
2034 let field_inits: Vec<String> = all_fields
2035 .iter()
2036 .map(|f| {
2037 if let Some(field) = variant_field_map.get(f.as_str()) {
2038 let has_binding = fields_with_binding_struct.contains(f.as_str());
2039 let is_mixed = mixed_named_fields.contains(f.as_str());
2040 if field.optional {
2041 match &field.ty {
2042 TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
2043 TypeRef::Named(_) if is_mixed => {
2044 format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
2046 }
2047 TypeRef::Named(_) if has_binding => {
2048 format!("{f}: {f}.map(|v| v.into())")
2049 }
2050 TypeRef::Named(_) => {
2053 format!("{f}: {f}.map(|v| v.into())")
2054 }
2055 _ => format!("{f}: {f}"),
2056 }
2057 } else if field.sanitized {
2058 format!("{f}: None")
2059 } else {
2060 match &field.ty {
2061 TypeRef::Named(_) if is_mixed => {
2062 format!("{f}: serde_json::to_string(&{f}).ok()")
2064 }
2065 TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
2066 TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
2069 TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
2070 TypeRef::Primitive(p) if needs_napi_cast(p) => {
2071 match p {
2072 alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
2073 alef_core::ir::PrimitiveType::U64
2074 | alef_core::ir::PrimitiveType::Usize
2075 | alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
2076 _ => format!("{f}: Some({f})"),
2078 }
2079 }
2080 _ => format!("{f}: Some({f})"),
2081 }
2082 }
2083 } else {
2084 format!("{f}: None")
2085 }
2086 })
2087 .collect();
2088 if is_tuple {
2089 writeln!(
2090 out,
2091 " {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2092 variant.name,
2093 destructured.join(", "),
2094 field_inits.join(", ")
2095 )
2096 .ok();
2097 } else {
2098 writeln!(
2099 out,
2100 " {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2101 variant.name,
2102 destructured.join(", "),
2103 field_inits.join(", ")
2104 )
2105 .ok();
2106 }
2107 }
2108 }
2109
2110 writeln!(out, " }}").ok();
2111 writeln!(out, " }}").ok();
2112 write!(out, "}}").ok();
2113 out
2114}
2115
2116fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
2120 use alef_core::ir::TypeRef;
2121 let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
2122
2123 for variant in &enum_def.variants {
2124 for field in &variant.fields {
2125 if field.sanitized {
2126 continue;
2127 }
2128 if let TypeRef::Named(n) = &field.ty {
2129 field_types.entry(&field.name).or_default().insert(n.as_str());
2130 }
2131 }
2132 }
2133
2134 field_types
2135 .into_iter()
2136 .filter(|(_, types)| types.len() > 1)
2137 .map(|(name, _)| name.to_string())
2138 .collect()
2139}
2140
2141fn tagged_enum_binding_struct_fields<'a>(
2147 enum_def: &'a EnumDef,
2148 struct_names: &ahash::AHashSet<String>,
2149) -> ahash::AHashSet<&'a str> {
2150 use alef_core::ir::TypeRef;
2151 let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
2152 let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
2153
2154 for variant in &enum_def.variants {
2155 for field in &variant.fields {
2156 if field.sanitized {
2157 sanitized_fields.insert(&field.name);
2158 }
2159 if let TypeRef::Named(n) = &field.ty {
2160 field_types.entry(&field.name).or_default().push(n);
2161 }
2162 }
2163 }
2164
2165 let mut result = ahash::AHashSet::new();
2166 for (field_name, types) in &field_types {
2167 if sanitized_fields.contains(field_name) {
2168 continue;
2169 }
2170 if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
2172 result.insert(*field_name);
2173 }
2174 }
2175 result
2176}
2177
2178#[cfg(test)]
2179mod tests {
2180 use super::*;
2181 use alef_core::ir::{ParamDef, TypeRef};
2182
2183 fn make_param(name: &str, optional: bool) -> ParamDef {
2184 ParamDef {
2185 name: name.to_string(),
2186 ty: TypeRef::String,
2187 optional,
2188 default: None,
2189 sanitized: false,
2190 typed_default: None,
2191 is_ref: false,
2192 is_mut: false,
2193 newtype_wrapper: None,
2194 original_type: None,
2195 }
2196 }
2197
2198 #[test]
2202 fn dts_params_reorders_required_after_optional() {
2203 let params = vec![
2204 make_param("ctx", false),
2205 make_param("lang", true),
2206 make_param("code", false),
2207 ];
2208 let result = dts_params(¶ms, "Js");
2209 let ctx_pos = result.find("ctx:").expect("ctx not found");
2211 let code_pos = result.find("code:").expect("code not found");
2212 let lang_pos = result.find("lang?:").expect("lang? not found");
2213 assert!(ctx_pos < lang_pos, "ctx should come before lang?: {result}");
2214 assert!(code_pos < lang_pos, "code should come before lang?: {result}");
2215 }
2216
2217 #[test]
2220 fn dts_params_preserves_already_valid_order() {
2221 let params = vec![
2222 make_param("ctx", false),
2223 make_param("code", false),
2224 make_param("lang", true),
2225 ];
2226 let result = dts_params(¶ms, "Js");
2227 assert_eq!(result, "ctx: string, code: string, lang?: string | undefined | null");
2228 }
2229
2230 #[test]
2232 fn dts_params_all_required_preserves_order() {
2233 let params = vec![make_param("a", false), make_param("b", false), make_param("c", false)];
2234 let result = dts_params(¶ms, "Js");
2235 assert_eq!(result, "a: string, b: string, c: string");
2236 }
2237}