use crate::type_map::NapiMapper;
use ahash::AHashSet;
use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
use alef_codegen::naming::to_node_name;
use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
use alef_codegen::type_mapper::TypeMapper;
use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile, PostBuildStep};
use alef_core::config::{AlefConfig, Language, resolve_output_dir};
use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
use std::path::PathBuf;
pub struct NapiBackend;
impl NapiBackend {
fn binding_config(core_import: &str) -> RustBindingConfig<'_> {
RustBindingConfig {
struct_attrs: &["napi"],
field_attrs: &[],
struct_derives: &["Clone"],
method_block_attr: Some("napi"),
constructor_attr: "#[napi(constructor)]",
static_attr: None,
function_attr: "#[napi]",
enum_attrs: &["napi(string_enum)"],
enum_derives: &["Clone"],
needs_signature: false,
signature_prefix: "",
signature_suffix: "",
core_import,
async_pattern: AsyncPattern::NapiNativeAsync,
has_serde: false,
type_name_prefix: "Js",
option_duration_on_defaults: true,
}
}
}
impl Backend for NapiBackend {
fn name(&self) -> &str {
"napi"
}
fn language(&self) -> Language {
Language::Node
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let mapper = NapiMapper;
let core_import = config.core_import();
let cfg = Self::binding_config(&core_import);
let mut builder = RustFileBuilder::new().with_generated_header();
builder.add_inner_attribute("allow(dead_code)");
builder.add_import("napi::*");
builder.add_import("napi_derive::napi");
for trait_path in generators::collect_trait_imports(api) {
builder.add_import(&trait_path);
}
let has_maps = api
.types
.iter()
.any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
|| api
.functions
.iter()
.any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
if has_maps {
builder.add_import("std::collections::HashMap");
}
let has_async =
api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
if has_async {
builder.add_item(&gen_tokio_runtime());
}
let opaque_types: AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
if !opaque_types.is_empty() {
builder.add_import("std::sync::Arc");
}
for typ in &api.types {
if typ.is_opaque {
builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(typ, &cfg, "Js"));
builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &cfg, &opaque_types));
} else {
builder.add_item(&gen_struct(typ, &mapper));
}
}
for enum_def in &api.enums {
builder.add_item(&gen_enum(enum_def));
}
for func in &api.functions {
builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types));
}
let binding_to_core = alef_codegen::conversions::convertible_types(api);
let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
let input_types = alef_codegen::conversions::input_type_names(api);
let napi_conv_config = alef_codegen::conversions::ConversionConfig {
type_name_prefix: "Js",
cast_large_ints_to_i64: true,
cast_f32_to_f64: true,
optionalize_defaults: true,
option_duration_on_defaults: true,
include_cfg_metadata: true,
..Default::default()
};
for typ in &api.types {
if input_types.contains(&typ.name)
&& alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
{
builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
typ,
&core_import,
&napi_conv_config,
));
}
if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
typ,
&core_import,
&opaque_types,
&napi_conv_config,
));
}
}
for e in &api.enums {
let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
if is_tagged_data_enum {
builder.add_item(&gen_tagged_enum_binding_to_core(e, &core_import));
builder.add_item(&gen_tagged_enum_core_to_binding(e, &core_import));
} else {
if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
e,
&core_import,
&napi_conv_config,
));
}
if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
e,
&core_import,
&napi_conv_config,
));
}
}
}
for error in &api.errors {
builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
}
let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
let content = builder.build();
let output_dir = resolve_output_dir(
config.output.node.as_ref(),
&config.crate_config.name,
"crates/{name}-node/src/",
);
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("lib.rs"),
content,
generated_header: false,
}])
}
fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let mut type_exports = vec![];
let mut function_exports = vec![];
for typ in &api.types {
type_exports.push(format!("Js{}", typ.name));
}
for enum_def in &api.enums {
function_exports.push(format!("Js{}", enum_def.name));
}
for func in &api.functions {
let js_name = to_node_name(&func.name);
function_exports.push(js_name);
}
type_exports.sort();
function_exports.sort();
let mut lines = vec![
"// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
"".to_string(),
];
if !function_exports.is_empty() {
lines.push("export {".to_string());
for name in &function_exports {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from '{}';", config.node_package_name()));
lines.push("".to_string());
}
if !type_exports.is_empty() {
lines.push("export type {".to_string());
for name in &type_exports {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from '{}';", config.node_package_name()));
}
let custom_mods = config.custom_modules.for_language(Language::Node);
for module_name in custom_mods {
lines.push(format!("export * from './{module_name}';"));
}
let content = lines.join("\n");
let output_path = PathBuf::from("packages/typescript/src/index.ts");
Ok(vec![GeneratedFile {
path: output_path,
content,
generated_header: false,
}])
}
fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let content = gen_dts(api);
let src_dir = resolve_output_dir(
config.output.node.as_ref(),
&config.crate_config.name,
"crates/{name}-node/src/",
);
let crate_root = {
let p = PathBuf::from(&src_dir);
match p.file_name().and_then(|n| n.to_str()) {
Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
_ => p,
}
};
Ok(vec![GeneratedFile {
path: crate_root.join("index.d.ts"),
content,
generated_header: false,
}])
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "napi",
crate_suffix: "-node",
depends_on_ffi: false,
post_build: vec![PostBuildStep::PatchFile {
path: "index.d.ts",
find: "export declare const enum",
replace: "export declare enum",
}],
})
}
}
fn gen_struct(typ: &TypeDef, mapper: &NapiMapper) -> String {
let mut struct_builder = StructBuilder::new(&format!("Js{}", typ.name));
struct_builder.add_attr("napi(object)");
struct_builder.add_derive("Clone");
if typ.has_default {
struct_builder.add_derive("Default");
}
for field in &typ.fields {
let mapped_type = mapper.map_type(&field.ty);
let field_type = if field.optional || typ.has_default {
format!("Option<{}>", mapped_type)
} else {
mapped_type
};
let js_name = to_node_name(&field.name);
let attrs = if js_name != field.name {
vec![format!("napi(js_name = \"{}\")", js_name)]
} else {
vec![]
};
struct_builder.add_field(&field.name, &field_type, attrs);
}
struct_builder.build()
}
fn gen_opaque_struct_methods(
typ: &TypeDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
) -> String {
let mut impl_builder = ImplBuilder::new(&format!("Js{}", typ.name));
impl_builder.add_attr("napi");
let (instance, statics) = partition_methods(&typ.methods);
for method in &instance {
impl_builder.add_method(&gen_opaque_instance_method(method, mapper, typ, cfg, opaque_types));
}
for method in &statics {
impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types));
}
impl_builder.build()
}
fn gen_opaque_instance_method(
method: &MethodDef,
mapper: &NapiMapper,
typ: &TypeDef,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
) -> String {
let params = function_params(&method.params, &|ty| mapper.map_type(ty));
let return_type = mapper.map_type(&method.return_type);
let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
let js_name = to_node_name(&method.name);
let js_name_attr = if js_name != method.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let async_kw = if method.is_async { "async " } else { "" };
let type_name = &typ.name;
let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
let call_args = napi_gen_call_args(&method.params, opaque_types);
let opaque_can_delegate = !method.sanitized
&& (!is_owned_receiver || typ.is_clone)
&& method
.params
.iter()
.all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
&& alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
let make_core_call = |method_name: &str| -> String {
if is_owned_receiver {
format!("(*self.inner).clone().{method_name}({call_args})")
} else {
format!("self.inner.{method_name}({call_args})")
}
};
let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
let async_result_wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
);
let body = if !opaque_can_delegate {
if cfg.has_serde
&& !method.sanitized
&& generators::has_named_params(&method.params, opaque_types)
&& method.error_type.is_some()
&& alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
{
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let serde_bindings =
generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, " ");
let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
let core_call = format!("self.inner.{}({serde_call_args})", method.name);
if matches!(method.return_type, TypeRef::Unit) {
format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
} else {
let wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
);
format!("{serde_bindings}let result = {core_call}{err_conv}?;\n Ok({wrap})")
}
} else {
generators::gen_unimplemented_body(
&method.return_type,
&format!("{type_name}.{}", method.name),
method.error_type.is_some(),
cfg,
&method.params,
)
}
} else if method.is_async {
let inner_clone_line = "let inner = self.inner.clone();\n ";
let core_call_str = make_async_core_call(&method.name);
generators::gen_async_body(
&core_call_str,
cfg,
method.error_type.is_some(),
&async_result_wrap,
true,
inner_clone_line,
matches!(method.return_type, TypeRef::Unit),
)
} else {
let core_call = make_core_call(&method.name);
if method.error_type.is_some() {
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
if matches!(method.return_type, TypeRef::Unit) {
format!("{core_call}{err_conv}?;\n Ok(())")
} else {
let wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
);
format!("let result = {core_call}{err_conv}?;\n Ok({wrap})")
}
} else {
napi_wrap_return(
&core_call,
&method.return_type,
type_name,
opaque_types,
true,
method.returns_ref,
)
}
};
let mut attrs = String::new();
if method.params.len() + 1 > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if method.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
if generators::is_trait_method_name(&method.name) {
attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n \
{body}\n}}",
method.name
)
}
fn gen_static_method(
method: &MethodDef,
mapper: &NapiMapper,
typ: &TypeDef,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
) -> String {
let params = function_params(&method.params, &|ty| mapper.map_type(ty));
let return_type = mapper.map_type(&method.return_type);
let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
let js_name = to_node_name(&method.name);
let js_name_attr = if js_name != method.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let type_name = &typ.name;
let core_type_path = typ.rust_path.replace('-', "_");
let call_args = napi_gen_call_args(&method.params, opaque_types);
let can_delegate_static = can_auto_delegate(method, opaque_types);
let async_kw = if method.is_async { "async " } else { "" };
let body = if !can_delegate_static {
generators::gen_unimplemented_body(
&method.return_type,
&format!("{type_name}::{}", method.name),
method.error_type.is_some(),
cfg,
&method.params,
)
} else if method.is_async {
let core_call = format!("{core_type_path}::{}({call_args})", method.name);
let return_wrap = napi_wrap_return(
"result",
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
);
generators::gen_async_body(
&core_call,
cfg,
method.error_type.is_some(),
&return_wrap,
false,
"",
matches!(method.return_type, TypeRef::Unit),
)
} else {
let core_call = format!("{core_type_path}::{}({call_args})", method.name);
if method.error_type.is_some() {
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let wrapped = napi_wrap_return(
"val",
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
);
if wrapped == "val" {
format!("{core_call}{err_conv}")
} else {
format!("{core_call}.map(|val| {wrapped}){err_conv}")
}
} else {
napi_wrap_return(
&core_call,
&method.return_type,
type_name,
opaque_types,
typ.is_opaque,
method.returns_ref,
)
}
};
let mut attrs = String::new();
if method.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if method.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
if generators::is_trait_method_name(&method.name) {
attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
{body}\n}}",
method.name
)
}
fn gen_enum(enum_def: &EnumDef) -> String {
let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
if is_tagged_data_enum {
return gen_tagged_enum_as_object(enum_def);
}
let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
"snake_case" => Some("snake_case"),
"camelCase" => Some("camelCase"),
"kebab-case" => Some("kebab-case"),
"SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
"lowercase" => Some("lowercase"),
"UPPERCASE" => Some("UPPERCASE"),
"PascalCase" => Some("PascalCase"),
_ => None,
});
let string_enum_attr = match napi_case {
Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
None => "#[napi(string_enum)]".to_string(),
};
let mut lines = vec![
string_enum_attr,
"#[derive(Clone)]".to_string(),
format!("pub enum Js{} {{", enum_def.name),
];
for variant in &enum_def.variants {
lines.push(format!(" {},", variant.name));
}
lines.push("}".to_string());
if let Some(first) = enum_def.variants.first() {
lines.push(String::new());
lines.push("#[allow(clippy::derivable_impls)]".to_string());
lines.push(format!("impl Default for Js{} {{", enum_def.name));
lines.push(format!(" fn default() -> Self {{ Self::{} }}", first.name));
lines.push("}".to_string());
}
lines.join("\n")
}
fn gen_tagged_enum_as_object(enum_def: &EnumDef) -> String {
use alef_codegen::type_mapper::TypeMapper;
let mapper = NapiMapper;
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let mut lines = vec![
"#[derive(Clone)]".to_string(),
"#[napi(object)]".to_string(),
format!("pub struct Js{} {{", enum_def.name),
format!(" #[napi(js_name = \"{tag_field}\")]"),
format!(" pub {tag_field}_tag: String,"),
];
let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for variant in &enum_def.variants {
for field in &variant.fields {
if seen_fields.insert(field.name.clone()) {
let field_type = mapper.map_type(&field.ty);
let js_name = alef_codegen::naming::to_node_name(&field.name);
if js_name != field.name {
lines.push(format!(" #[napi(js_name = \"{js_name}\")]"));
}
lines.push(format!(" pub {}: Option<{field_type}>,", field.name));
}
}
}
lines.push("}".to_string());
lines.push(String::new());
lines.push("#[allow(clippy::derivable_impls)]".to_string());
lines.push(format!("impl Default for Js{} {{", enum_def.name));
lines.push(format!(
" fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
seen_fields
.iter()
.map(|f| format!("{f}: None"))
.collect::<Vec<_>>()
.join(", ")
));
lines.push("}".to_string());
lines.join("\n")
}
fn gen_function(
func: &FunctionDef,
mapper: &NapiMapper,
cfg: &RustBindingConfig,
opaque_types: &AHashSet<String>,
) -> String {
let params = function_params(&func.params, &|ty| {
if let TypeRef::Named(n) = ty {
if opaque_types.contains(n.as_str()) {
return format!("&Js{n}");
}
}
mapper.map_type(ty)
});
let return_type = mapper.map_type(&func.return_type);
let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
let js_name = to_node_name(&func.name);
let js_name_attr = if js_name != func.name {
format!("(js_name = \"{}\")", js_name)
} else {
String::new()
};
let core_import = cfg.core_import;
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let use_let_bindings = generators::has_named_params(&func.params, opaque_types);
let call_args = if use_let_bindings {
generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
} else {
napi_gen_call_args(&func.params, opaque_types)
};
let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
let async_kw = if func.is_async { "async " } else { "" };
let body = if !can_delegate_fn {
if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
let serde_bindings =
generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, " ");
let core_call = format!("{core_fn_path}({call_args})");
if matches!(func.return_type, TypeRef::Unit) {
format!("{serde_bindings}{core_call}{err_conv}?;\n Ok(())")
} else {
let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
if wrapped == "val" {
format!("{serde_bindings}{core_call}{err_conv}")
} else {
format!("{serde_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
}
}
} else {
generators::gen_unimplemented_body(
&func.return_type,
&func.name,
func.error_type.is_some(),
cfg,
&func.params,
)
}
} else if func.is_async {
let core_call = format!("{core_fn_path}({call_args})");
let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref);
generators::gen_async_body(
&core_call,
cfg,
func.error_type.is_some(),
&return_wrap,
false,
"",
matches!(func.return_type, TypeRef::Unit),
)
} else {
let core_call = format!("{core_fn_path}({call_args})");
let let_bindings = if use_let_bindings {
generators::gen_named_let_bindings_pub(&func.params, opaque_types)
} else {
String::new()
};
if func.error_type.is_some() {
let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref);
if wrapped == "val" {
format!("{let_bindings}{core_call}{err_conv}")
} else {
format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
}
} else {
format!(
"{let_bindings}{}",
napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref)
)
}
};
let mut attrs = String::new();
if func.params.len() > 7 {
attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
}
if func.error_type.is_some() {
attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
}
format!(
"{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n \
{body}\n}}",
func.name
)
}
fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
params
.iter()
.map(|p| match &p.ty {
TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
let core_ty = core_prim_str(prim);
if p.optional {
format!("{}.map(|v| v as {})", p.name, core_ty)
} else {
format!("{} as {}", p.name, core_ty)
}
}
TypeRef::Duration => {
if p.optional {
format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
} else {
format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
}
}
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if p.optional {
format!("{}.as_ref().map(|v| &v.inner)", p.name)
} else {
format!("&{}.inner", p.name)
}
}
TypeRef::Named(_) => {
if p.optional {
format!("{}.map(Into::into)", p.name)
} else {
format!("{}.into()", p.name)
}
}
TypeRef::String | TypeRef::Char => format!("&{}", p.name),
TypeRef::Path => format!("std::path::PathBuf::from({})", p.name),
TypeRef::Bytes => format!("&{}", p.name),
_ => p.name.clone(),
})
.collect::<Vec<_>>()
.join(", ")
}
fn napi_wrap_return(
expr: &str,
return_type: &TypeRef,
type_name: &str,
opaque_types: &AHashSet<String>,
self_is_opaque: bool,
returns_ref: bool,
) -> String {
match return_type {
TypeRef::Primitive(p) if needs_napi_cast(p) => {
format!("{expr} as i64")
}
TypeRef::Duration => format!("{expr}.as_millis() as i64"),
TypeRef::Named(n) if n == type_name && self_is_opaque => {
if returns_ref {
format!("Self {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("Self {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if returns_ref {
format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("Js{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.clone().into()")
} else {
format!("{expr}.into()")
}
}
_ => generators::wrap_return(
expr,
return_type,
type_name,
opaque_types,
self_is_opaque,
returns_ref,
false,
),
}
}
fn napi_wrap_return_fn(
expr: &str,
return_type: &TypeRef,
opaque_types: &AHashSet<String>,
returns_ref: bool,
) -> String {
match return_type {
TypeRef::Primitive(p) if needs_napi_cast(p) => {
format!("{expr} as i64")
}
TypeRef::Duration => format!("{expr}.as_millis() as i64"),
TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
if returns_ref {
format!("Js{n} {{ inner: Arc::new({expr}.clone()) }}")
} else {
format!("Js{n} {{ inner: Arc::new({expr}) }}")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.clone().into()")
} else {
format!("{expr}.into()")
}
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.into()")
} else {
expr.to_string()
}
}
TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
TypeRef::Json => format!("{expr}.to_string()"),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if returns_ref {
format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v.clone()) }})")
} else {
format!("{expr}.map(|v| Js{name} {{ inner: Arc::new(v) }})")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.map(|v| v.clone().into())")
} else {
format!("{expr}.map(Into::into)")
}
}
TypeRef::Path => {
format!("{expr}.map(Into::into)")
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.map(Into::into)")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
if returns_ref {
format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v.clone()) }}).collect()")
} else {
format!("{expr}.into_iter().map(|v| Js{name} {{ inner: Arc::new(v) }}).collect()")
}
}
TypeRef::Named(_) => {
if returns_ref {
format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
} else {
format!("{expr}.into_iter().map(Into::into).collect()")
}
}
TypeRef::Path => {
format!("{expr}.into_iter().map(Into::into).collect()")
}
TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
if returns_ref {
format!("{expr}.into_iter().map(Into::into).collect()")
} else {
expr.to_string()
}
}
_ => expr.to_string(),
},
_ => expr.to_string(),
}
}
fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
matches!(
p,
alef_core::ir::PrimitiveType::U64 | alef_core::ir::PrimitiveType::Usize | alef_core::ir::PrimitiveType::Isize
)
}
fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
match p {
alef_core::ir::PrimitiveType::U64 => "u64",
alef_core::ir::PrimitiveType::Usize => "usize",
alef_core::ir::PrimitiveType::Isize => "isize",
_ => unreachable!(),
}
}
fn gen_tokio_runtime() -> String {
"static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect(\"Failed to create Tokio runtime\")
});"
.to_string()
}
fn gen_dts(api: &ApiSurface) -> String {
let mut lines: Vec<String> = vec![
"/* auto-generated by alef */".to_string(),
"/* eslint-disable */".to_string(),
];
let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
plain_types.sort_by(|a, b| a.name.cmp(&b.name));
let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
enum Decl<'a> {
Class(&'a TypeDef),
Interface(&'a TypeDef),
Enum(&'a EnumDef),
Function(&'a FunctionDef),
}
let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
for t in &opaque_types {
all_decls.push((format!("Js{}", t.name), Decl::Class(t)));
}
for t in &plain_types {
all_decls.push((format!("Js{}", t.name), Decl::Interface(t)));
}
for e in &sorted_enums {
all_decls.push((format!("Js{}", e.name), Decl::Enum(e)));
}
for f in &sorted_fns {
all_decls.push((to_node_name(&f.name), Decl::Function(f)));
}
all_decls.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
for (_, decl) in &all_decls {
lines.push(String::new());
match decl {
Decl::Class(typ) => {
lines.push(format!("export declare class Js{} {{", typ.name));
for method in &typ.methods {
let js_name = to_node_name(&method.name);
let params = dts_params(&method.params);
let ret = dts_return_type(&method.return_type, method.error_type.is_some(), method.is_async);
if method.is_static {
lines.push(format!(" static {js_name}({params}): {ret}"));
} else {
lines.push(format!(" {js_name}({params}): {ret}"));
}
}
lines.push("}".to_string());
}
Decl::Interface(typ) => {
lines.push(format!("export interface Js{} {{", typ.name));
for field in &typ.fields {
let js_name = to_node_name(&field.name);
let ts_ty = dts_type(&field.ty);
lines.push(format!(" {js_name}?: {ts_ty}"));
}
lines.push("}".to_string());
}
Decl::Enum(e) => {
lines.push(format!("export declare enum Js{} {{", e.name));
for variant in &e.variants {
let value = variant.serde_rename.as_deref().unwrap_or(variant.name.as_str());
lines.push(format!(" {} = '{}',", variant.name, value));
}
lines.push("}".to_string());
}
Decl::Function(func) => {
let js_name = to_node_name(&func.name);
let params = dts_params(&func.params);
let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async);
lines.push(format!("export declare function {js_name}({params}): {ret}"));
}
}
}
lines.push(String::new());
lines.join("\n")
}
fn dts_type(ty: &TypeRef) -> String {
match ty {
TypeRef::Primitive(p) => match p {
alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
alef_core::ir::PrimitiveType::U8
| alef_core::ir::PrimitiveType::U16
| alef_core::ir::PrimitiveType::U32
| alef_core::ir::PrimitiveType::I8
| alef_core::ir::PrimitiveType::I16
| alef_core::ir::PrimitiveType::I32
| alef_core::ir::PrimitiveType::F32
| alef_core::ir::PrimitiveType::F64 => "number".to_string(),
alef_core::ir::PrimitiveType::U64
| alef_core::ir::PrimitiveType::I64
| alef_core::ir::PrimitiveType::Usize
| alef_core::ir::PrimitiveType::Isize => "number".to_string(),
},
TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
TypeRef::Bytes => "Uint8Array".to_string(),
TypeRef::Json => "string".to_string(),
TypeRef::Duration => "number".to_string(),
TypeRef::Unit => "void".to_string(),
TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner)),
TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner)),
TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k), dts_type(v)),
TypeRef::Named(name) => format!("Js{name}"),
}
}
fn dts_params(params: &[ParamDef]) -> String {
params
.iter()
.map(|p| {
let js_name = to_node_name(&p.name);
let ts_ty = dts_type(&p.ty);
if p.optional {
format!("{js_name}?: {ts_ty} | undefined | null")
} else {
format!("{js_name}: {ts_ty}")
}
})
.collect::<Vec<_>>()
.join(", ")
}
fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool) -> String {
let base = match ret {
TypeRef::Unit => "void".to_string(),
other => dts_type(other),
};
if is_async { format!("Promise<{base}>") } else { base }
}
fn gen_tagged_enum_binding_to_core(enum_def: &EnumDef, core_import: &str) -> String {
use alef_core::ir::TypeRef;
use std::fmt::Write;
let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
let binding_name = format!("Js{}", enum_def.name);
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let mut out = String::with_capacity(512);
writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
writeln!(out, " fn from(val: {binding_name}) -> Self {{").ok();
writeln!(out, " match val.{tag_field}_tag.as_str() {{").ok();
for variant in &enum_def.variants {
let default_tag = variant.name.to_lowercase();
let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
if variant.fields.is_empty() {
writeln!(out, " \"{tag_value}\" => Self::{},", variant.name).ok();
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let field_exprs: Vec<String> = variant
.fields
.iter()
.map(|f| {
if f.optional {
format!("val.{}", f.name)
} else if f.sanitized {
"Default::default()".to_string()
} else {
match &f.ty {
TypeRef::Named(_) => {
format!("val.{}.unwrap_or_default().into()", f.name)
}
_ => {
format!("val.{}.unwrap_or_default()", f.name)
}
}
}
})
.collect();
if is_tuple {
writeln!(
out,
" \"{tag_value}\" => Self::{}({}),",
variant.name,
field_exprs.join(", ")
)
.ok();
} else {
let field_inits: Vec<String> = variant
.fields
.iter()
.zip(field_exprs.iter())
.map(|(f, expr)| format!("{}: {expr}", f.name))
.collect();
writeln!(
out,
" \"{tag_value}\" => Self::{} {{ {} }},",
variant.name,
field_inits.join(", ")
)
.ok();
}
}
}
if let Some(first) = enum_def.variants.first() {
if first.fields.is_empty() {
writeln!(out, " _ => Self::{},", first.name).ok();
} else {
let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
if is_tuple {
let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
writeln!(out, " _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
} else {
let defaults: Vec<String> = first
.fields
.iter()
.map(|f| format!("{}: Default::default()", f.name))
.collect();
writeln!(
out,
" _ => Self::{} {{ {} }},",
first.name,
defaults.join(", ")
)
.ok();
}
}
}
writeln!(out, " }}").ok();
writeln!(out, " }}").ok();
write!(out, "}}").ok();
out
}
fn gen_tagged_enum_core_to_binding(enum_def: &EnumDef, core_import: &str) -> String {
use std::fmt::Write;
let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
let binding_name = format!("Js{}", enum_def.name);
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let all_fields: Vec<String> = {
let mut fields = std::collections::BTreeSet::new();
for v in &enum_def.variants {
for f in &v.fields {
fields.insert(f.name.clone());
}
}
fields.into_iter().collect()
};
let mut out = String::with_capacity(512);
writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
writeln!(out, " fn from(val: {core_path}) -> Self {{").ok();
writeln!(out, " match val {{").ok();
for variant in &enum_def.variants {
let default_tag = variant.name.to_lowercase();
let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
let _variant_field_names: std::collections::BTreeSet<String> =
variant.fields.iter().map(|f| f.name.clone()).collect();
if variant.fields.is_empty() {
writeln!(
out,
" {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
all_fields
.iter()
.map(|f| format!("{f}: None"))
.collect::<Vec<_>>()
.join(", ")
)
.ok();
} else {
use alef_core::ir::TypeRef;
let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
let destructured: Vec<String> = variant
.fields
.iter()
.map(|f| {
if f.sanitized {
if is_tuple {
format!("_{}", f.name)
} else {
format!("{}: _{}", f.name, f.name)
}
} else {
f.name.clone()
}
})
.collect();
let field_inits: Vec<String> = all_fields
.iter()
.map(|f| {
if let Some(field) = variant_field_map.get(f.as_str()) {
if field.optional {
format!("{f}: {f}")
} else if field.sanitized {
format!("{f}: None")
} else {
match &field.ty {
TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
_ => format!("{f}: Some({f})"),
}
}
} else {
format!("{f}: None")
}
})
.collect();
if is_tuple {
writeln!(
out,
" {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
destructured.join(", "),
field_inits.join(", ")
)
.ok();
} else {
writeln!(
out,
" {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
variant.name,
destructured.join(", "),
field_inits.join(", ")
)
.ok();
}
}
}
writeln!(out, " }}").ok();
writeln!(out, " }}").ok();
write!(out, "}}").ok();
out
}