Skip to main content

weaveffi_gen_node/
lib.rs

1use anyhow::Result;
2use camino::Utf8Path;
3use heck::ToUpperCamelCase;
4use weaveffi_core::codegen::Generator;
5use weaveffi_core::config::GeneratorConfig;
6use weaveffi_core::utils::{c_abi_struct_name, local_type_name, wrapper_name};
7use weaveffi_ir::ir::{Api, Function, Module, StructDef, TypeRef};
8
9pub struct NodeGenerator;
10
11impl NodeGenerator {
12    fn generate_impl(
13        &self,
14        api: &Api,
15        out_dir: &Utf8Path,
16        package_name: &str,
17        strip_module_prefix: bool,
18    ) -> Result<()> {
19        let dir = out_dir.join("node");
20        std::fs::create_dir_all(&dir)?;
21        std::fs::write(
22            dir.join("index.js"),
23            "module.exports = require('./index.node')\n",
24        )?;
25        std::fs::write(
26            dir.join("types.d.ts"),
27            render_node_dts(api, strip_module_prefix),
28        )?;
29        std::fs::write(dir.join("package.json"), render_package_json(package_name))?;
30        std::fs::write(dir.join("binding.gyp"), render_binding_gyp())?;
31        std::fs::write(
32            dir.join("weaveffi_addon.c"),
33            render_addon_c(api, strip_module_prefix),
34        )?;
35        Ok(())
36    }
37}
38
39impl Generator for NodeGenerator {
40    fn name(&self) -> &'static str {
41        "node"
42    }
43
44    fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()> {
45        self.generate_impl(api, out_dir, "weaveffi", true)
46    }
47
48    fn generate_with_config(
49        &self,
50        api: &Api,
51        out_dir: &Utf8Path,
52        config: &GeneratorConfig,
53    ) -> Result<()> {
54        self.generate_impl(
55            api,
56            out_dir,
57            config.node_package_name(),
58            config.strip_module_prefix,
59        )
60    }
61
62    fn output_files(&self, _api: &Api, out_dir: &Utf8Path) -> Vec<String> {
63        vec![
64            out_dir.join("node/index.js").to_string(),
65            out_dir.join("node/types.d.ts").to_string(),
66            out_dir.join("node/package.json").to_string(),
67            out_dir.join("node/binding.gyp").to_string(),
68            out_dir.join("node/weaveffi_addon.c").to_string(),
69        ]
70    }
71}
72
73fn render_package_json(name: &str) -> String {
74    format!(
75        r#"{{
76  "name": "{name}",
77  "version": "0.1.0",
78  "main": "index.js",
79  "types": "types.d.ts",
80  "gypfile": true,
81  "scripts": {{
82    "install": "node-gyp rebuild"
83  }}
84}}
85"#
86    )
87}
88
89fn render_binding_gyp() -> String {
90    r#"{
91  "targets": [
92    {
93      "target_name": "weaveffi",
94      "sources": ["weaveffi_addon.c"],
95      "include_dirs": ["../c"],
96      "libraries": ["-lweaveffi"]
97    }
98  ]
99}
100"#
101    .to_string()
102}
103
104fn is_c_ptr_type(ty: &TypeRef) -> bool {
105    matches!(
106        ty,
107        TypeRef::StringUtf8
108            | TypeRef::Bytes
109            | TypeRef::Struct(_)
110            | TypeRef::List(_)
111            | TypeRef::Map(_, _)
112            | TypeRef::Iterator(_)
113    )
114}
115
116fn c_elem_type(ty: &TypeRef, module: &str) -> String {
117    match ty {
118        TypeRef::I32 => "int32_t".into(),
119        TypeRef::U32 => "uint32_t".into(),
120        TypeRef::I64 => "int64_t".into(),
121        TypeRef::F64 => "double".into(),
122        TypeRef::Bool => "bool".into(),
123        TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
124        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
125        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
126        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, "weaveffi")),
127        TypeRef::Enum(e) => format!("weaveffi_{module}_{e}"),
128        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
129            c_elem_type(inner, module)
130        }
131        TypeRef::Map(_, _) => "void*".into(),
132        TypeRef::Callback(_) => todo!("callback Node type"),
133    }
134}
135
136fn c_ret_type_str(ty: &TypeRef, module: &str) -> String {
137    match ty {
138        TypeRef::I32 => "int32_t".into(),
139        TypeRef::U32 => "uint32_t".into(),
140        TypeRef::I64 => "int64_t".into(),
141        TypeRef::F64 => "double".into(),
142        TypeRef::Bool => "bool".into(),
143        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
144        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
145        TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
146        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, "weaveffi")),
147        TypeRef::Enum(e) => format!("weaveffi_{module}_{e}"),
148        TypeRef::Optional(inner) => {
149            if is_c_ptr_type(inner) {
150                c_ret_type_str(inner, module)
151            } else {
152                format!("{}*", c_elem_type(inner, module))
153            }
154        }
155        TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module)),
156        TypeRef::Map(_, _) => "void".into(),
157        TypeRef::Iterator(_) => "void*".into(),
158        TypeRef::Callback(_) => todo!("callback Node type"),
159    }
160}
161
162fn napi_getter(ty: &TypeRef) -> &'static str {
163    match ty {
164        TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
165        TypeRef::U32 => "napi_get_value_uint32",
166        TypeRef::I64 | TypeRef::Handle | TypeRef::TypedHandle(_) | TypeRef::Struct(_) => {
167            "napi_get_value_int64"
168        }
169        TypeRef::F64 => "napi_get_value_double",
170        TypeRef::Bool => "napi_get_value_bool",
171        _ => "napi_get_value_int64",
172    }
173}
174
175fn collect_all_modules(modules: &[Module]) -> Vec<&Module> {
176    let mut all = Vec::new();
177    for m in modules {
178        all.push(m);
179        all.extend(collect_all_modules(&m.modules));
180    }
181    all
182}
183
184fn collect_modules_with_path(modules: &[Module]) -> Vec<(&Module, String)> {
185    let mut result = Vec::new();
186    for m in modules {
187        collect_module_with_path(m, &m.name, &mut result);
188    }
189    result
190}
191
192fn collect_module_with_path<'a>(m: &'a Module, path: &str, out: &mut Vec<(&'a Module, String)>) {
193    out.push((m, path.to_string()));
194    for sub in &m.modules {
195        collect_module_with_path(sub, &format!("{path}_{}", sub.name), out);
196    }
197}
198
199fn render_addon_c(api: &Api, strip_module_prefix: bool) -> String {
200    let mut out = String::from(
201        "#include <node_api.h>\n#include \"weaveffi.h\"\n#include <stdlib.h>\n#include <string.h>\n\n",
202    );
203
204    let has_async = collect_all_modules(&api.modules)
205        .iter()
206        .any(|m| m.functions.iter().any(|f| f.r#async));
207    if has_async {
208        out.push_str("typedef struct {\n");
209        out.push_str("    napi_env env;\n");
210        out.push_str("    napi_deferred deferred;\n");
211        out.push_str("} weaveffi_napi_async_ctx;\n\n");
212    }
213
214    let mut all_exports: Vec<(String, String)> = Vec::new();
215
216    for (m, path) in collect_modules_with_path(&api.modules) {
217        for f in &m.functions {
218            let c_name = format!("weaveffi_{}_{}", path, f.name);
219            let napi_name = format!("Napi_{c_name}");
220            let js_name = wrapper_name(&path, &f.name, strip_module_prefix);
221            all_exports.push((js_name, napi_name.clone()));
222
223            if f.r#async {
224                render_async_callback(&mut out, f, &c_name, &path);
225            }
226
227            out.push_str(&format!(
228                "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
229            ));
230            if f.r#async {
231                render_async_napi_body(&mut out, f, &c_name, &path);
232            } else {
233                render_napi_body(&mut out, f, &c_name, &path);
234            }
235            out.push_str("}\n\n");
236        }
237    }
238
239    out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
240    if !all_exports.is_empty() {
241        out.push_str("  napi_property_descriptor props[] = {\n");
242        for (js_name, napi_fn) in &all_exports {
243            out.push_str(&format!(
244                "    {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
245            ));
246        }
247        out.push_str("  };\n");
248        out.push_str(&format!(
249            "  napi_define_properties(env, exports, {}, props);\n",
250            all_exports.len()
251        ));
252    }
253    out.push_str("  return exports;\n");
254    out.push_str("}\n\n");
255    out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n");
256    out
257}
258
259fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str) -> String {
260    match ret {
261        None => String::new(),
262        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
263        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
264            ", const uint8_t* result, size_t result_len".into()
265        }
266        Some(TypeRef::List(inner)) => {
267            let et = c_elem_type(inner, module);
268            format!(", {et}* result, size_t result_len")
269        }
270        Some(TypeRef::Map(k, v)) => {
271            let kt = c_elem_type(k, module);
272            let vt = c_elem_type(v, module);
273            format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
274        }
275        Some(t) => format!(", {} result", c_ret_type_str(t, module)),
276    }
277}
278
279fn emit_async_resolve_value(out: &mut String, ret: Option<&TypeRef>) {
280    out.push_str("        napi_value val;\n");
281    match ret {
282        None => out.push_str("        napi_get_undefined(ctx->env, &val);\n"),
283        Some(TypeRef::I32) => out.push_str("        napi_create_int32(ctx->env, result, &val);\n"),
284        Some(TypeRef::U32) => out.push_str("        napi_create_uint32(ctx->env, result, &val);\n"),
285        Some(TypeRef::I64) => out.push_str("        napi_create_int64(ctx->env, result, &val);\n"),
286        Some(TypeRef::F64) => out.push_str("        napi_create_double(ctx->env, result, &val);\n"),
287        Some(TypeRef::Bool) => out.push_str("        napi_get_boolean(ctx->env, result, &val);\n"),
288        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
289            out.push_str(
290                "        napi_create_string_utf8(ctx->env, result, NAPI_AUTO_LENGTH, &val);\n",
291            );
292        }
293        Some(TypeRef::TypedHandle(_) | TypeRef::Handle) => {
294            out.push_str("        napi_create_int64(ctx->env, (int64_t)result, &val);\n");
295        }
296        Some(TypeRef::Struct(_)) => {
297            out.push_str("        napi_create_int64(ctx->env, (int64_t)(intptr_t)result, &val);\n");
298        }
299        Some(TypeRef::Enum(_)) => {
300            out.push_str("        napi_create_int32(ctx->env, (int32_t)result, &val);\n");
301        }
302        Some(TypeRef::Iterator(_)) => {
303            out.push_str("        napi_create_int64(ctx->env, (int64_t)(intptr_t)result, &val);\n");
304        }
305        _ => out.push_str("        napi_get_undefined(ctx->env, &val);\n"),
306    }
307    out.push_str("        napi_resolve_deferred(ctx->env, ctx->deferred, val);\n");
308}
309
310fn render_async_callback(out: &mut String, f: &Function, c_name: &str, module: &str) {
311    let cb_name = format!("{c_name}_napi_cb");
312    let cb_result = async_cb_result_params_node(f.returns.as_ref(), module);
313
314    out.push_str(&format!(
315        "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
316    ));
317    out.push_str("    weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)context;\n");
318    out.push_str("    if (err != NULL && err->code != 0) {\n");
319    out.push_str("        napi_value err_msg;\n");
320    out.push_str(
321        "        napi_create_string_utf8(ctx->env, err->message, NAPI_AUTO_LENGTH, &err_msg);\n",
322    );
323    out.push_str("        napi_reject_deferred(ctx->env, ctx->deferred, err_msg);\n");
324    out.push_str("    } else {\n");
325    emit_async_resolve_value(out, f.returns.as_ref());
326    out.push_str("    }\n");
327    out.push_str("    free(ctx);\n");
328    out.push_str("}\n\n");
329}
330
331fn render_async_napi_body(out: &mut String, f: &Function, c_name: &str, module: &str) {
332    let n = f.params.len();
333    if n > 0 {
334        out.push_str(&format!("  size_t argc = {n};\n"));
335        out.push_str(&format!("  napi_value args[{n}];\n"));
336        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
337    } else {
338        out.push_str("  size_t argc = 0;\n");
339        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
340    }
341
342    let mut c_args: Vec<String> = Vec::new();
343    let mut cleanups: Vec<String> = Vec::new();
344    for (i, p) in f.params.iter().enumerate() {
345        emit_param(out, &mut c_args, &mut cleanups, &p.ty, &p.name, i, module);
346    }
347
348    out.push_str(
349        "  weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)malloc(sizeof(weaveffi_napi_async_ctx));\n",
350    );
351    out.push_str("  ctx->env = env;\n");
352    out.push_str("  napi_value promise;\n");
353    out.push_str("  napi_create_promise(env, &ctx->deferred, &promise);\n");
354
355    if f.cancellable {
356        c_args.push("NULL".into());
357    }
358
359    let cb_name = format!("{c_name}_napi_cb");
360    c_args.push(cb_name);
361    c_args.push("ctx".into());
362    let args_str = c_args.join(", ");
363    out.push_str(&format!("  {c_name}_async({args_str});\n"));
364
365    for cleanup in &cleanups {
366        out.push_str(cleanup);
367    }
368
369    out.push_str("  return promise;\n");
370}
371
372fn render_napi_body(out: &mut String, f: &Function, c_name: &str, module: &str) {
373    let n = f.params.len();
374    if n > 0 {
375        out.push_str(&format!("  size_t argc = {n};\n"));
376        out.push_str(&format!("  napi_value args[{n}];\n"));
377        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
378    } else {
379        out.push_str("  size_t argc = 0;\n");
380        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
381    }
382
383    let mut c_args: Vec<String> = Vec::new();
384    let mut cleanups: Vec<String> = Vec::new();
385    for (i, p) in f.params.iter().enumerate() {
386        emit_param(out, &mut c_args, &mut cleanups, &p.ty, &p.name, i, module);
387    }
388
389    out.push_str("  weaveffi_error err = {0};\n");
390
391    if let Some(ret) = &f.returns {
392        emit_ret_out_params(out, &mut c_args, ret, module);
393    }
394    c_args.push("&err".to_string());
395
396    let args_str = c_args.join(", ");
397    let ret_type = f.returns.as_ref().map(|r| c_ret_type_str(r, module));
398    match &ret_type {
399        Some(rt) if rt != "void" => {
400            out.push_str(&format!("  {rt} result = {c_name}({args_str});\n"));
401        }
402        _ => {
403            out.push_str(&format!("  {c_name}({args_str});\n"));
404        }
405    }
406
407    for cleanup in &cleanups {
408        out.push_str(cleanup);
409    }
410
411    out.push_str("  if (err.code != 0) {\n");
412    out.push_str("    napi_throw_error(env, NULL, err.message);\n");
413    out.push_str("    weaveffi_error_clear(&err);\n");
414    out.push_str("    return NULL;\n");
415    out.push_str("  }\n");
416
417    match &f.returns {
418        Some(ret) => emit_ret_to_napi(out, ret, module, &f.name),
419        None => {
420            out.push_str("  napi_value ret;\n");
421            out.push_str("  napi_get_undefined(env, &ret);\n");
422            out.push_str("  return ret;\n");
423        }
424    }
425}
426
427fn emit_param(
428    out: &mut String,
429    c_args: &mut Vec<String>,
430    cleanups: &mut Vec<String>,
431    ty: &TypeRef,
432    name: &str,
433    idx: usize,
434    module: &str,
435) {
436    match ty {
437        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
438            let ct = c_elem_type(ty, module);
439            let getter = napi_getter(ty);
440            out.push_str(&format!("  {ct} {name};\n"));
441            out.push_str(&format!("  {getter}(env, args[{idx}], &{name});\n"));
442            c_args.push(name.into());
443        }
444        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
445            out.push_str(&format!("  size_t {name}_len;\n"));
446            out.push_str(&format!(
447                "  napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
448            ));
449            out.push_str(&format!(
450                "  char* {name} = (char*)malloc({name}_len + 1);\n"
451            ));
452            out.push_str(&format!(
453                "  napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
454            ));
455            c_args.push(name.into());
456            cleanups.push(format!("  free({name});\n"));
457        }
458        TypeRef::TypedHandle(_) | TypeRef::Handle => {
459            out.push_str(&format!("  int64_t {name}_raw;\n"));
460            out.push_str(&format!(
461                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
462            ));
463            c_args.push(format!("(weaveffi_handle_t){name}_raw"));
464        }
465        TypeRef::Enum(e) => {
466            out.push_str(&format!("  int32_t {name};\n"));
467            out.push_str(&format!(
468                "  napi_get_value_int32(env, args[{idx}], &{name});\n"
469            ));
470            c_args.push(format!("(weaveffi_{module}_{e}){name}"));
471        }
472        TypeRef::Struct(s) => {
473            let abi = c_abi_struct_name(s, module, "weaveffi");
474            out.push_str(&format!("  int64_t {name}_raw;\n"));
475            out.push_str(&format!(
476                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
477            ));
478            c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
479        }
480        TypeRef::Optional(inner) => {
481            out.push_str(&format!("  napi_valuetype {name}_type;\n"));
482            out.push_str(&format!("  napi_typeof(env, args[{idx}], &{name}_type);\n"));
483            emit_optional_param(out, c_args, cleanups, inner, name, idx, module);
484        }
485        TypeRef::List(inner) => {
486            emit_list_param(out, c_args, cleanups, inner, name, idx, module);
487        }
488        TypeRef::Bytes | TypeRef::BorrowedBytes => {
489            out.push_str(&format!("  void* {name}_raw;\n"));
490            out.push_str(&format!("  size_t {name}_len;\n"));
491            out.push_str(&format!(
492                "  napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
493            ));
494            c_args.push(format!("(const uint8_t*){name}_raw"));
495            c_args.push(format!("{name}_len"));
496        }
497        TypeRef::Map(k, v) => {
498            emit_map_param(out, c_args, cleanups, k, v, name, idx, module);
499        }
500        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
501        TypeRef::Callback(_) => todo!("callback Node param"),
502    }
503}
504
505fn emit_opt_val(
506    out: &mut String,
507    c_args: &mut Vec<String>,
508    c_type: &str,
509    napi_fn: &str,
510    name: &str,
511    idx: usize,
512) {
513    out.push_str(&format!("  {c_type} {name}_val;\n"));
514    out.push_str(&format!("  const {c_type}* {name}_ptr = NULL;\n"));
515    out.push_str(&format!(
516        "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
517    ));
518    out.push_str(&format!("    {napi_fn}(env, args[{idx}], &{name}_val);\n"));
519    out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
520    out.push_str("  }\n");
521    c_args.push(format!("{name}_ptr"));
522}
523
524fn emit_optional_param(
525    out: &mut String,
526    c_args: &mut Vec<String>,
527    cleanups: &mut Vec<String>,
528    inner: &TypeRef,
529    name: &str,
530    idx: usize,
531    module: &str,
532) {
533    match inner {
534        TypeRef::I32 => {
535            emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
536        }
537        TypeRef::U32 => {
538            emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
539        }
540        TypeRef::I64 => {
541            emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
542        }
543        TypeRef::F64 => {
544            emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
545        }
546        TypeRef::Bool => {
547            emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
548        }
549        TypeRef::TypedHandle(_) | TypeRef::Handle => {
550            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
551            out.push_str(&format!("  weaveffi_handle_t {name}_val;\n"));
552            out.push_str(&format!("  const weaveffi_handle_t* {name}_ptr = NULL;\n"));
553            out.push_str(&format!(
554                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
555            ));
556            out.push_str(&format!(
557                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
558            ));
559            out.push_str(&format!(
560                "    {name}_val = (weaveffi_handle_t){name}_raw;\n"
561            ));
562            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
563            out.push_str("  }\n");
564            c_args.push(format!("{name}_ptr"));
565        }
566        TypeRef::Enum(e) => {
567            let etype = format!("weaveffi_{module}_{e}");
568            out.push_str(&format!("  int32_t {name}_raw;\n"));
569            out.push_str(&format!("  {etype} {name}_val;\n"));
570            out.push_str(&format!("  const {etype}* {name}_ptr = NULL;\n"));
571            out.push_str(&format!(
572                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
573            ));
574            out.push_str(&format!(
575                "    napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
576            ));
577            out.push_str(&format!("    {name}_val = ({etype}){name}_raw;\n"));
578            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
579            out.push_str("  }\n");
580            c_args.push(format!("{name}_ptr"));
581        }
582        TypeRef::StringUtf8 => {
583            out.push_str(&format!("  char* {name} = NULL;\n"));
584            out.push_str(&format!(
585                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
586            ));
587            out.push_str(&format!("    size_t {name}_len;\n"));
588            out.push_str(&format!(
589                "    napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
590            ));
591            out.push_str(&format!("    {name} = (char*)malloc({name}_len + 1);\n"));
592            out.push_str(&format!(
593                "    napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
594            ));
595            out.push_str("  }\n");
596            c_args.push(name.into());
597            cleanups.push(format!("  free({name});\n"));
598        }
599        TypeRef::Struct(s) => {
600            let abi = c_abi_struct_name(s, module, "weaveffi");
601            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
602            out.push_str(&format!(
603                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
604            ));
605            out.push_str(&format!(
606                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
607            ));
608            out.push_str("  }\n");
609            c_args.push(format!(
610                "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
611            ));
612        }
613        _ => {
614            emit_param(out, c_args, cleanups, inner, name, idx, module);
615        }
616    }
617}
618
619fn emit_list_param(
620    out: &mut String,
621    c_args: &mut Vec<String>,
622    cleanups: &mut Vec<String>,
623    inner: &TypeRef,
624    name: &str,
625    idx: usize,
626    module: &str,
627) {
628    let et = c_elem_type(inner, module);
629    out.push_str(&format!("  uint32_t {name}_count;\n"));
630    out.push_str(&format!(
631        "  napi_get_array_length(env, args[{idx}], &{name}_count);\n"
632    ));
633    out.push_str(&format!(
634        "  {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
635    ));
636    out.push_str(&format!(
637        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
638    ));
639    out.push_str(&format!("    napi_value {name}_el;\n"));
640    out.push_str(&format!(
641        "    napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
642    ));
643
644    match inner {
645        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
646            let getter = napi_getter(inner);
647            out.push_str(&format!(
648                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
649            ));
650        }
651        TypeRef::TypedHandle(_) | TypeRef::Handle => {
652            out.push_str(&format!("    int64_t {name}_h;\n"));
653            out.push_str(&format!(
654                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
655            ));
656            out.push_str(&format!(
657                "    {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
658            ));
659        }
660        TypeRef::Enum(_) => {
661            out.push_str(&format!("    int32_t {name}_ev;\n"));
662            out.push_str(&format!(
663                "    napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
664            ));
665            out.push_str(&format!("    {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
666        }
667        TypeRef::StringUtf8 => {
668            out.push_str(&format!("    size_t {name}_sl;\n"));
669            out.push_str(&format!(
670                "    napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
671            ));
672            out.push_str(&format!(
673                "    char* {name}_s = (char*)malloc({name}_sl + 1);\n"
674            ));
675            out.push_str(&format!(
676                "    napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
677            ));
678            out.push_str(&format!("    {name}_arr[{name}_i] = {name}_s;\n"));
679        }
680        TypeRef::Struct(_) => {
681            out.push_str(&format!("    int64_t {name}_sp;\n"));
682            out.push_str(&format!(
683                "    napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
684            ));
685            out.push_str(&format!(
686                "    {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
687            ));
688        }
689        _ => {
690            let getter = napi_getter(inner);
691            out.push_str(&format!(
692                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
693            ));
694        }
695    }
696
697    out.push_str("  }\n");
698    c_args.push(format!("{name}_arr"));
699    c_args.push(format!("(size_t){name}_count"));
700
701    if matches!(inner, TypeRef::StringUtf8) {
702        cleanups.push(format!(
703            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
704        ));
705    }
706    cleanups.push(format!("  free({name}_arr);\n"));
707}
708
709#[allow(clippy::too_many_arguments)]
710fn emit_map_param(
711    out: &mut String,
712    c_args: &mut Vec<String>,
713    cleanups: &mut Vec<String>,
714    k: &TypeRef,
715    v: &TypeRef,
716    name: &str,
717    idx: usize,
718    module: &str,
719) {
720    let kt = c_elem_type(k, module);
721    let vt = c_elem_type(v, module);
722    out.push_str(&format!("  napi_value {name}_keys_napi;\n"));
723    out.push_str(&format!(
724        "  napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
725    ));
726    out.push_str(&format!("  uint32_t {name}_count;\n"));
727    out.push_str(&format!(
728        "  napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
729    ));
730    out.push_str(&format!(
731        "  {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
732    ));
733    out.push_str(&format!(
734        "  {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
735    ));
736    out.push_str(&format!(
737        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
738    ));
739    out.push_str(&format!("    napi_value {name}_k;\n"));
740    out.push_str(&format!(
741        "    napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
742    ));
743
744    if matches!(k, TypeRef::StringUtf8) {
745        out.push_str(&format!("    size_t {name}_kl;\n"));
746        out.push_str(&format!(
747            "    napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
748        ));
749        out.push_str(&format!(
750            "    char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
751        ));
752        out.push_str(&format!(
753            "    napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
754        ));
755        out.push_str(&format!("    {name}_keys[{name}_i] = {name}_ks;\n"));
756    } else {
757        out.push_str(&format!("    napi_value {name}_kn;\n"));
758        out.push_str(&format!(
759            "    napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
760        ));
761        let kgetter = napi_getter(k);
762        out.push_str(&format!(
763            "    {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
764        ));
765    }
766
767    out.push_str(&format!("    napi_value {name}_v;\n"));
768    out.push_str(&format!(
769        "    napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
770    ));
771
772    if matches!(v, TypeRef::StringUtf8) {
773        out.push_str(&format!("    size_t {name}_vl;\n"));
774        out.push_str(&format!(
775            "    napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
776        ));
777        out.push_str(&format!(
778            "    char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
779        ));
780        out.push_str(&format!(
781            "    napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
782        ));
783        out.push_str(&format!("    {name}_values[{name}_i] = {name}_vs;\n"));
784    } else {
785        let vgetter = napi_getter(v);
786        out.push_str(&format!(
787            "    {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
788        ));
789    }
790
791    out.push_str("  }\n");
792    c_args.push(format!("{name}_keys"));
793    c_args.push(format!("{name}_values"));
794    c_args.push(format!("(size_t){name}_count"));
795
796    if matches!(k, TypeRef::StringUtf8) {
797        cleanups.push(format!(
798            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
799        ));
800    }
801    cleanups.push(format!("  free({name}_keys);\n"));
802    if matches!(v, TypeRef::StringUtf8) {
803        cleanups.push(format!(
804            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
805        ));
806    }
807    cleanups.push(format!("  free({name}_values);\n"));
808}
809
810fn emit_ret_out_params(out: &mut String, c_args: &mut Vec<String>, ty: &TypeRef, module: &str) {
811    match ty {
812        TypeRef::Bytes | TypeRef::List(_) => {
813            out.push_str("  size_t out_len;\n");
814            c_args.push("&out_len".into());
815        }
816        TypeRef::Map(k, v) => {
817            let kt = c_elem_type(k, module);
818            let vt = c_elem_type(v, module);
819            out.push_str(&format!("  {kt}* out_keys = NULL;\n"));
820            out.push_str(&format!("  {vt}* out_values = NULL;\n"));
821            out.push_str("  size_t out_len = 0;\n");
822            c_args.push("out_keys".into());
823            c_args.push("out_values".into());
824            c_args.push("&out_len".into());
825        }
826        TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
827            emit_ret_out_params(out, c_args, inner, module);
828        }
829        _ => {}
830    }
831}
832
833fn emit_ret_to_napi(out: &mut String, ty: &TypeRef, module: &str, fn_name: &str) {
834    out.push_str("  napi_value ret;\n");
835    match ty {
836        TypeRef::I32 => out.push_str("  napi_create_int32(env, result, &ret);\n"),
837        TypeRef::U32 => out.push_str("  napi_create_uint32(env, result, &ret);\n"),
838        TypeRef::I64 => out.push_str("  napi_create_int64(env, result, &ret);\n"),
839        TypeRef::F64 => out.push_str("  napi_create_double(env, result, &ret);\n"),
840        TypeRef::Bool => out.push_str("  napi_get_boolean(env, result, &ret);\n"),
841        TypeRef::StringUtf8 => {
842            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
843            out.push_str("  weaveffi_free_string(result);\n");
844        }
845        TypeRef::BorrowedStr => {
846            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
847        }
848        TypeRef::TypedHandle(_) | TypeRef::Handle => {
849            out.push_str("  napi_create_int64(env, (int64_t)result, &ret);\n");
850        }
851        TypeRef::Struct(_) => {
852            out.push_str("  napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n");
853        }
854        TypeRef::Enum(_) => {
855            out.push_str("  napi_create_int32(env, (int32_t)result, &ret);\n");
856        }
857        TypeRef::Bytes => {
858            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
859            out.push_str("  weaveffi_free_bytes((uint8_t*)result, out_len);\n");
860        }
861        TypeRef::BorrowedBytes => {
862            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
863        }
864        TypeRef::Optional(inner) => {
865            out.push_str("  if (result == NULL) {\n");
866            out.push_str("    napi_get_null(env, &ret);\n");
867            out.push_str("  } else {\n");
868            emit_optional_ret_inner(out, inner, module);
869            out.push_str("  }\n");
870        }
871        TypeRef::List(inner) => emit_list_ret(out, inner, module, "  "),
872        TypeRef::Map(_, _) => {
873            out.push_str("  napi_create_object(env, &ret);\n");
874        }
875        TypeRef::Iterator(inner) => {
876            let fn_pascal = fn_name.to_upper_camel_case();
877            let iter_type = format!("weaveffi_{module}_{fn_pascal}Iterator");
878            let et = c_elem_type(inner, module);
879            out.push_str("  napi_create_array(env, &ret);\n");
880            out.push_str("  uint32_t iter_idx = 0;\n");
881            out.push_str(&format!("  {et} iter_item;\n"));
882            out.push_str(&format!(
883                "  while ({iter_type}_next(result, &iter_item)) {{\n"
884            ));
885            out.push_str("    napi_value elem;\n");
886            match inner.as_ref() {
887                TypeRef::I32 => {
888                    out.push_str("    napi_create_int32(env, iter_item, &elem);\n");
889                }
890                TypeRef::U32 => {
891                    out.push_str("    napi_create_uint32(env, iter_item, &elem);\n");
892                }
893                TypeRef::I64 => {
894                    out.push_str("    napi_create_int64(env, iter_item, &elem);\n");
895                }
896                TypeRef::F64 => {
897                    out.push_str("    napi_create_double(env, iter_item, &elem);\n");
898                }
899                TypeRef::Bool => {
900                    out.push_str("    napi_get_boolean(env, iter_item, &elem);\n");
901                }
902                TypeRef::TypedHandle(_) | TypeRef::Handle => {
903                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
904                }
905                TypeRef::StringUtf8 => {
906                    out.push_str(
907                        "    napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
908                    );
909                    out.push_str("    weaveffi_free_string(iter_item);\n");
910                }
911                TypeRef::Struct(_) | TypeRef::Enum(_) => {
912                    out.push_str(
913                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
914                    );
915                }
916                _ => {
917                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
918                }
919            }
920            out.push_str("    napi_set_element(env, ret, iter_idx++, elem);\n");
921            out.push_str("  }\n");
922            out.push_str(&format!("  {iter_type}_destroy(result);\n"));
923        }
924        TypeRef::Callback(_) => todo!("callback Node return"),
925    }
926    out.push_str("  return ret;\n");
927}
928
929fn emit_optional_ret_inner(out: &mut String, inner: &TypeRef, module: &str) {
930    match inner {
931        TypeRef::I32 => {
932            out.push_str("    napi_create_int32(env, *result, &ret);\n");
933            out.push_str("    free(result);\n");
934        }
935        TypeRef::U32 => {
936            out.push_str("    napi_create_uint32(env, *result, &ret);\n");
937            out.push_str("    free(result);\n");
938        }
939        TypeRef::I64 => {
940            out.push_str("    napi_create_int64(env, *result, &ret);\n");
941            out.push_str("    free(result);\n");
942        }
943        TypeRef::F64 => {
944            out.push_str("    napi_create_double(env, *result, &ret);\n");
945            out.push_str("    free(result);\n");
946        }
947        TypeRef::Bool => {
948            out.push_str("    napi_get_boolean(env, *result, &ret);\n");
949            out.push_str("    free(result);\n");
950        }
951        TypeRef::TypedHandle(_) | TypeRef::Handle => {
952            out.push_str("    napi_create_int64(env, (int64_t)*result, &ret);\n");
953            out.push_str("    free(result);\n");
954        }
955        TypeRef::Enum(_) => {
956            out.push_str("    napi_create_int32(env, (int32_t)*result, &ret);\n");
957            out.push_str("    free(result);\n");
958        }
959        TypeRef::StringUtf8 => {
960            out.push_str("    napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
961            out.push_str("    weaveffi_free_string(result);\n");
962        }
963        TypeRef::Struct(_) => {
964            out.push_str("    napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n");
965        }
966        TypeRef::List(li) => emit_list_ret(out, li, module, "    "),
967        _ => out.push_str("    napi_get_null(env, &ret);\n"),
968    }
969}
970
971fn emit_list_ret(out: &mut String, inner: &TypeRef, _module: &str, ind: &str) {
972    out.push_str(&format!(
973        "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
974    ));
975    out.push_str(&format!(
976        "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
977    ));
978    out.push_str(&format!("{ind}  napi_value elem;\n"));
979    match inner {
980        TypeRef::I32 => out.push_str(&format!(
981            "{ind}  napi_create_int32(env, result[ret_i], &elem);\n"
982        )),
983        TypeRef::U32 => out.push_str(&format!(
984            "{ind}  napi_create_uint32(env, result[ret_i], &elem);\n"
985        )),
986        TypeRef::I64 => out.push_str(&format!(
987            "{ind}  napi_create_int64(env, result[ret_i], &elem);\n"
988        )),
989        TypeRef::F64 => out.push_str(&format!(
990            "{ind}  napi_create_double(env, result[ret_i], &elem);\n"
991        )),
992        TypeRef::Bool => out.push_str(&format!(
993            "{ind}  napi_get_boolean(env, result[ret_i], &elem);\n"
994        )),
995        TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
996            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
997        )),
998        TypeRef::StringUtf8 => {
999            out.push_str(&format!(
1000                "{ind}  napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
1001            ));
1002            out.push_str(&format!("{ind}  weaveffi_free_string(result[ret_i]);\n"));
1003        }
1004        TypeRef::Struct(_) | TypeRef::Enum(_) => out.push_str(&format!(
1005            "{ind}  napi_create_int64(env, (int64_t)(intptr_t)result[ret_i], &elem);\n"
1006        )),
1007        _ => out.push_str(&format!(
1008            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1009        )),
1010    }
1011    out.push_str(&format!(
1012        "{ind}  napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
1013    ));
1014    out.push_str(&format!("{ind}}}\n"));
1015    out.push_str(&format!("{ind}free(result);\n"));
1016}
1017
1018fn ts_type_for(ty: &TypeRef) -> String {
1019    match ty {
1020        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
1021        TypeRef::Bool => "boolean".into(),
1022        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
1023        TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
1024        TypeRef::Handle => "bigint".into(),
1025        TypeRef::TypedHandle(name) => name.clone(),
1026        TypeRef::Struct(name) => local_type_name(name).to_string(),
1027        TypeRef::Enum(name) => name.clone(),
1028        TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
1029        TypeRef::List(inner) => {
1030            let inner_ts = ts_type_for(inner);
1031            if matches!(inner.as_ref(), TypeRef::Optional(_)) {
1032                format!("({inner_ts})[]")
1033            } else {
1034                format!("{inner_ts}[]")
1035            }
1036        }
1037        TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
1038        TypeRef::Iterator(inner) => {
1039            let t = ts_type_for(inner);
1040            format!("{t}[]")
1041        }
1042        TypeRef::Callback(_) => todo!("callback Node type"),
1043    }
1044}
1045
1046fn render_struct_builder_dts(out: &mut String, s: &StructDef) {
1047    let name = &s.name;
1048    out.push_str(&format!("export interface {}Builder {{\n", s.name));
1049    for field in &s.fields {
1050        let method = format!("with{}", field.name.to_upper_camel_case());
1051        let ts = ts_type_for(&field.ty);
1052        out.push_str(&format!("  {method}(value: {ts}): {name}Builder;\n"));
1053    }
1054    out.push_str(&format!("  build(): {name};\n"));
1055    out.push_str("}\n");
1056}
1057
1058fn render_node_dts(api: &Api, strip_module_prefix: bool) -> String {
1059    let mut out = String::from("// Generated types for WeaveFFI functions\n");
1060    for (m, path) in collect_modules_with_path(&api.modules) {
1061        for s in &m.structs {
1062            out.push_str(&format!("export interface {} {{\n", s.name));
1063            for field in &s.fields {
1064                out.push_str(&format!("  {}: {};\n", field.name, ts_type_for(&field.ty)));
1065            }
1066            out.push_str("}\n");
1067            if s.builder {
1068                render_struct_builder_dts(&mut out, s);
1069            }
1070        }
1071        for e in &m.enums {
1072            out.push_str(&format!("export enum {} {{\n", e.name));
1073            for v in &e.variants {
1074                out.push_str(&format!("  {} = {},\n", v.name, v.value));
1075            }
1076            out.push_str("}\n");
1077        }
1078        out.push_str(&format!("// module {}\n", path));
1079        for f in &m.functions {
1080            let params: Vec<String> = f
1081                .params
1082                .iter()
1083                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
1084                .collect();
1085            let base_ret = match &f.returns {
1086                Some(ty) => ts_type_for(ty),
1087                None => "void".into(),
1088            };
1089            let ret = if f.r#async {
1090                format!("Promise<{base_ret}>")
1091            } else {
1092                base_ret
1093            };
1094            let ts_name = wrapper_name(&path, &f.name, strip_module_prefix);
1095            out.push_str(&format!(
1096                "/** Maps to C function: weaveffi_{}_{} */\n",
1097                path, f.name
1098            ));
1099            if let Some(msg) = &f.deprecated {
1100                out.push_str(&format!("/** @deprecated {} */\n", msg));
1101            }
1102            out.push_str(&format!(
1103                "export function {}({}): {}\n",
1104                ts_name,
1105                params.join(", "),
1106                ret
1107            ));
1108        }
1109    }
1110    out
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115    use super::*;
1116    use weaveffi_core::config::GeneratorConfig;
1117    use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
1118
1119    fn make_api(modules: Vec<Module>) -> Api {
1120        Api {
1121            version: "0.1.0".into(),
1122            modules,
1123            generators: None,
1124        }
1125    }
1126
1127    fn make_module(name: &str) -> Module {
1128        Module {
1129            name: name.into(),
1130            functions: vec![],
1131            structs: vec![],
1132            enums: vec![],
1133            callbacks: vec![],
1134            listeners: vec![],
1135            errors: None,
1136            modules: vec![],
1137        }
1138    }
1139
1140    #[test]
1141    fn ts_type_for_primitives() {
1142        assert_eq!(ts_type_for(&TypeRef::I32), "number");
1143        assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
1144        assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
1145        assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
1146        assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
1147    }
1148
1149    #[test]
1150    fn ts_type_for_struct_and_enum() {
1151        assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
1152        assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
1153    }
1154
1155    #[test]
1156    fn ts_type_for_optional() {
1157        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1158        assert_eq!(ts_type_for(&ty), "string | null");
1159    }
1160
1161    #[test]
1162    fn ts_type_for_list() {
1163        let ty = TypeRef::List(Box::new(TypeRef::I32));
1164        assert_eq!(ts_type_for(&ty), "number[]");
1165    }
1166
1167    #[test]
1168    fn ts_type_for_list_of_optional() {
1169        let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
1170        assert_eq!(ts_type_for(&ty), "(number | null)[]");
1171    }
1172
1173    #[test]
1174    fn ts_type_for_map() {
1175        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1176        assert_eq!(ts_type_for(&ty), "Record<string, number>");
1177    }
1178
1179    #[test]
1180    fn ts_type_for_optional_list() {
1181        let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
1182        assert_eq!(ts_type_for(&ty), "number[] | null");
1183    }
1184
1185    #[test]
1186    fn generate_node_dts_with_structs() {
1187        let mut m = make_module("contacts");
1188        m.structs.push(StructDef {
1189            name: "Contact".into(),
1190            doc: None,
1191            fields: vec![
1192                StructField {
1193                    name: "name".into(),
1194                    ty: TypeRef::StringUtf8,
1195                    doc: None,
1196                    default: None,
1197                },
1198                StructField {
1199                    name: "age".into(),
1200                    ty: TypeRef::I32,
1201                    doc: None,
1202                    default: None,
1203                },
1204                StructField {
1205                    name: "active".into(),
1206                    ty: TypeRef::Bool,
1207                    doc: None,
1208                    default: None,
1209                },
1210            ],
1211            builder: false,
1212        });
1213        m.enums.push(EnumDef {
1214            name: "Color".into(),
1215            doc: None,
1216            variants: vec![
1217                EnumVariant {
1218                    name: "Red".into(),
1219                    value: 0,
1220                    doc: None,
1221                },
1222                EnumVariant {
1223                    name: "Green".into(),
1224                    value: 1,
1225                    doc: None,
1226                },
1227                EnumVariant {
1228                    name: "Blue".into(),
1229                    value: 2,
1230                    doc: None,
1231                },
1232            ],
1233        });
1234        m.functions.push(Function {
1235            name: "get_contact".into(),
1236            params: vec![Param {
1237                name: "id".into(),
1238                ty: TypeRef::I32,
1239                mutable: false,
1240            }],
1241            returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1242                "Contact".into(),
1243            )))),
1244            doc: None,
1245            r#async: false,
1246            cancellable: false,
1247            deprecated: None,
1248            since: None,
1249        });
1250        m.functions.push(Function {
1251            name: "list_contacts".into(),
1252            params: vec![],
1253            returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1254            doc: None,
1255            r#async: false,
1256            cancellable: false,
1257            deprecated: None,
1258            since: None,
1259        });
1260
1261        let dts = render_node_dts(&make_api(vec![m]), true);
1262
1263        assert!(dts.contains("export interface Contact {"));
1264        assert!(dts.contains("  name: string;"));
1265        assert!(dts.contains("  age: number;"));
1266        assert!(dts.contains("  active: boolean;"));
1267        assert!(dts.contains("export enum Color {"));
1268        assert!(dts.contains("  Red = 0,"));
1269        assert!(dts.contains("  Green = 1,"));
1270        assert!(dts.contains("  Blue = 2,"));
1271        assert!(dts.contains("export function get_contact(id: number): Contact | null"));
1272        assert!(dts.contains("export function list_contacts(): Contact[]"));
1273
1274        let iface_pos = dts.find("export interface Contact").unwrap();
1275        let enum_pos = dts.find("export enum Color").unwrap();
1276        let fn_pos = dts.find("export function get_contact").unwrap();
1277        assert!(
1278            iface_pos < fn_pos,
1279            "interface should appear before functions"
1280        );
1281        assert!(enum_pos < fn_pos, "enum should appear before functions");
1282    }
1283
1284    #[test]
1285    fn node_generates_binding_gyp() {
1286        let api = make_api(vec![{
1287            let mut m = make_module("math");
1288            m.functions.push(Function {
1289                name: "add".into(),
1290                params: vec![
1291                    Param {
1292                        name: "a".into(),
1293                        ty: TypeRef::I32,
1294                        mutable: false,
1295                    },
1296                    Param {
1297                        name: "b".into(),
1298                        ty: TypeRef::I32,
1299                        mutable: false,
1300                    },
1301                ],
1302                returns: Some(TypeRef::I32),
1303                doc: None,
1304                r#async: false,
1305                cancellable: false,
1306                deprecated: None,
1307                since: None,
1308            });
1309            m
1310        }]);
1311
1312        let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
1313        let _ = std::fs::remove_dir_all(&tmp);
1314        std::fs::create_dir_all(&tmp).unwrap();
1315        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1316
1317        NodeGenerator.generate(&api, out_dir).unwrap();
1318
1319        let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
1320        assert!(
1321            gyp.contains("\"target_name\": \"weaveffi\""),
1322            "missing target_name: {gyp}"
1323        );
1324        assert!(
1325            gyp.contains("weaveffi_addon.c"),
1326            "missing source file: {gyp}"
1327        );
1328
1329        let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
1330        assert!(
1331            addon.contains("napi_value Init("),
1332            "missing Init function: {addon}"
1333        );
1334        assert!(
1335            addon.contains("weaveffi_math_add"),
1336            "missing C ABI call: {addon}"
1337        );
1338        assert!(
1339            addon.contains("napi_get_cb_info"),
1340            "missing napi_get_cb_info call: {addon}"
1341        );
1342
1343        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1344        assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
1345        assert!(
1346            pkg.contains("node-gyp rebuild"),
1347            "missing install script: {pkg}"
1348        );
1349
1350        let _ = std::fs::remove_dir_all(&tmp);
1351    }
1352
1353    #[test]
1354    fn generate_node_dts_with_structs_and_enums() {
1355        let api = make_api(vec![Module {
1356            name: "contacts".to_string(),
1357            functions: vec![
1358                Function {
1359                    name: "get_contact".to_string(),
1360                    params: vec![Param {
1361                        name: "id".to_string(),
1362                        ty: TypeRef::I32,
1363                        mutable: false,
1364                    }],
1365                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1366                        "Contact".into(),
1367                    )))),
1368                    doc: None,
1369                    r#async: false,
1370                    cancellable: false,
1371                    deprecated: None,
1372                    since: None,
1373                },
1374                Function {
1375                    name: "list_contacts".to_string(),
1376                    params: vec![],
1377                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1378                    doc: None,
1379                    r#async: false,
1380                    cancellable: false,
1381                    deprecated: None,
1382                    since: None,
1383                },
1384                Function {
1385                    name: "set_favorite_color".to_string(),
1386                    params: vec![
1387                        Param {
1388                            name: "contact_id".to_string(),
1389                            ty: TypeRef::I32,
1390                            mutable: false,
1391                        },
1392                        Param {
1393                            name: "color".to_string(),
1394                            ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
1395                            mutable: false,
1396                        },
1397                    ],
1398                    returns: None,
1399                    doc: None,
1400                    r#async: false,
1401                    cancellable: false,
1402                    deprecated: None,
1403                    since: None,
1404                },
1405                Function {
1406                    name: "get_tags".to_string(),
1407                    params: vec![Param {
1408                        name: "contact_id".to_string(),
1409                        ty: TypeRef::I32,
1410                        mutable: false,
1411                    }],
1412                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
1413                    doc: None,
1414                    r#async: false,
1415                    cancellable: false,
1416                    deprecated: None,
1417                    since: None,
1418                },
1419            ],
1420            structs: vec![StructDef {
1421                name: "Contact".to_string(),
1422                doc: None,
1423                fields: vec![
1424                    StructField {
1425                        name: "name".to_string(),
1426                        ty: TypeRef::StringUtf8,
1427                        doc: None,
1428                        default: None,
1429                    },
1430                    StructField {
1431                        name: "email".to_string(),
1432                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1433                        doc: None,
1434                        default: None,
1435                    },
1436                    StructField {
1437                        name: "tags".to_string(),
1438                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
1439                        doc: None,
1440                        default: None,
1441                    },
1442                ],
1443                builder: false,
1444            }],
1445            enums: vec![EnumDef {
1446                name: "Color".to_string(),
1447                doc: None,
1448                variants: vec![
1449                    EnumVariant {
1450                        name: "Red".to_string(),
1451                        value: 0,
1452                        doc: None,
1453                    },
1454                    EnumVariant {
1455                        name: "Green".to_string(),
1456                        value: 1,
1457                        doc: None,
1458                    },
1459                    EnumVariant {
1460                        name: "Blue".to_string(),
1461                        value: 2,
1462                        doc: None,
1463                    },
1464                ],
1465            }],
1466            callbacks: vec![],
1467            listeners: vec![],
1468            errors: None,
1469            modules: vec![],
1470        }]);
1471
1472        let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
1473        let _ = std::fs::remove_dir_all(&tmp);
1474        std::fs::create_dir_all(&tmp).unwrap();
1475        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1476
1477        NodeGenerator.generate(&api, out_dir).unwrap();
1478
1479        let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
1480
1481        assert!(
1482            dts.contains("export interface Contact {"),
1483            "missing Contact interface: {dts}"
1484        );
1485        assert!(dts.contains("  name: string;"), "missing name field: {dts}");
1486        assert!(
1487            dts.contains("  email: string | null;"),
1488            "missing optional email field: {dts}"
1489        );
1490        assert!(
1491            dts.contains("  tags: string[];"),
1492            "missing list tags field: {dts}"
1493        );
1494
1495        assert!(
1496            dts.contains("export enum Color {"),
1497            "missing Color enum: {dts}"
1498        );
1499        assert!(dts.contains("  Red = 0,"), "missing Red variant: {dts}");
1500        assert!(dts.contains("  Green = 1,"), "missing Green variant: {dts}");
1501        assert!(dts.contains("  Blue = 2,"), "missing Blue variant: {dts}");
1502
1503        assert!(
1504            dts.contains("export function get_contact(id: number): Contact | null"),
1505            "missing get_contact with optional return: {dts}"
1506        );
1507        assert!(
1508            dts.contains("export function list_contacts(): Contact[]"),
1509            "missing list_contacts with list return: {dts}"
1510        );
1511        assert!(
1512            dts.contains(
1513                "export function set_favorite_color(contact_id: number, color: Color | null): void"
1514            ),
1515            "missing set_favorite_color with optional enum param: {dts}"
1516        );
1517        assert!(
1518            dts.contains("export function get_tags(contact_id: number): string[]"),
1519            "missing get_tags with list return: {dts}"
1520        );
1521
1522        let iface_pos = dts.find("export interface Contact").unwrap();
1523        let enum_pos = dts.find("export enum Color").unwrap();
1524        let fn_pos = dts.find("export function get_contact").unwrap();
1525        assert!(
1526            iface_pos < fn_pos,
1527            "interface should appear before functions"
1528        );
1529        assert!(enum_pos < fn_pos, "enum should appear before functions");
1530
1531        let _ = std::fs::remove_dir_all(&tmp);
1532    }
1533
1534    #[test]
1535    fn node_custom_package_name() {
1536        let api = make_api(vec![make_module("math")]);
1537
1538        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
1539        let _ = std::fs::remove_dir_all(&tmp);
1540        std::fs::create_dir_all(&tmp).unwrap();
1541        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1542
1543        let config = GeneratorConfig {
1544            node_package_name: Some("@myorg/cool-lib".into()),
1545            ..GeneratorConfig::default()
1546        };
1547        NodeGenerator
1548            .generate_with_config(&api, out_dir, &config)
1549            .unwrap();
1550
1551        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1552        assert!(
1553            pkg.contains("\"name\": \"@myorg/cool-lib\""),
1554            "package.json should use custom name: {pkg}"
1555        );
1556        assert!(
1557            !pkg.contains("\"name\": \"weaveffi\""),
1558            "package.json should not contain default name: {pkg}"
1559        );
1560
1561        let _ = std::fs::remove_dir_all(&tmp);
1562    }
1563
1564    #[test]
1565    fn node_dts_has_jsdoc() {
1566        let api = make_api(vec![{
1567            let mut m = make_module("math");
1568            m.functions.push(Function {
1569                name: "add".into(),
1570                params: vec![
1571                    Param {
1572                        name: "a".into(),
1573                        ty: TypeRef::I32,
1574                        mutable: false,
1575                    },
1576                    Param {
1577                        name: "b".into(),
1578                        ty: TypeRef::I32,
1579                        mutable: false,
1580                    },
1581                ],
1582                returns: Some(TypeRef::I32),
1583                doc: None,
1584                r#async: false,
1585                cancellable: false,
1586                deprecated: None,
1587                since: None,
1588            });
1589            m.functions.push(Function {
1590                name: "subtract".into(),
1591                params: vec![
1592                    Param {
1593                        name: "a".into(),
1594                        ty: TypeRef::I32,
1595                        mutable: false,
1596                    },
1597                    Param {
1598                        name: "b".into(),
1599                        ty: TypeRef::I32,
1600                        mutable: false,
1601                    },
1602                ],
1603                returns: Some(TypeRef::I32),
1604                doc: None,
1605                r#async: false,
1606                cancellable: false,
1607                deprecated: None,
1608                since: None,
1609            });
1610            m
1611        }]);
1612
1613        let dts = render_node_dts(&api, true);
1614
1615        assert!(
1616            dts.contains("/** Maps to C function: weaveffi_math_add */\nexport function add("),
1617            "missing JSDoc for add: {dts}"
1618        );
1619        assert!(
1620            dts.contains(
1621                "/** Maps to C function: weaveffi_math_subtract */\nexport function subtract("
1622            ),
1623            "missing JSDoc for subtract: {dts}"
1624        );
1625    }
1626
1627    #[test]
1628    fn node_addon_has_no_todo() {
1629        let api = make_api(vec![{
1630            let mut m = make_module("math");
1631            m.functions.push(Function {
1632                name: "add".into(),
1633                params: vec![
1634                    Param {
1635                        name: "a".into(),
1636                        ty: TypeRef::I32,
1637                        mutable: false,
1638                    },
1639                    Param {
1640                        name: "b".into(),
1641                        ty: TypeRef::I32,
1642                        mutable: false,
1643                    },
1644                ],
1645                returns: Some(TypeRef::I32),
1646                doc: None,
1647                r#async: false,
1648                cancellable: false,
1649                deprecated: None,
1650                since: None,
1651            });
1652            m
1653        }]);
1654        let addon = render_addon_c(&api, true);
1655        assert!(
1656            !addon.contains("// TODO: implement"),
1657            "generated addon.c should not contain TODO comments: {addon}"
1658        );
1659    }
1660
1661    #[test]
1662    fn node_addon_extracts_args() {
1663        let api = make_api(vec![{
1664            let mut m = make_module("math");
1665            m.functions.push(Function {
1666                name: "add".into(),
1667                params: vec![
1668                    Param {
1669                        name: "a".into(),
1670                        ty: TypeRef::I32,
1671                        mutable: false,
1672                    },
1673                    Param {
1674                        name: "b".into(),
1675                        ty: TypeRef::I32,
1676                        mutable: false,
1677                    },
1678                ],
1679                returns: Some(TypeRef::I32),
1680                doc: None,
1681                r#async: false,
1682                cancellable: false,
1683                deprecated: None,
1684                since: None,
1685            });
1686            m
1687        }]);
1688        let addon = render_addon_c(&api, true);
1689        assert!(
1690            addon.contains("napi_get_cb_info"),
1691            "generated addon.c should call napi_get_cb_info: {addon}"
1692        );
1693    }
1694
1695    #[test]
1696    fn node_addon_frees_strings() {
1697        let api = make_api(vec![{
1698            let mut m = make_module("greet");
1699            m.functions.push(Function {
1700                name: "hello".into(),
1701                params: vec![Param {
1702                    name: "name".into(),
1703                    ty: TypeRef::StringUtf8,
1704                    mutable: false,
1705                }],
1706                returns: Some(TypeRef::StringUtf8),
1707                doc: None,
1708                r#async: false,
1709                cancellable: false,
1710                deprecated: None,
1711                since: None,
1712            });
1713            m
1714        }]);
1715        let addon = render_addon_c(&api, true);
1716        assert!(
1717            addon.contains("weaveffi_free_string(result)"),
1718            "generated addon should free returned strings: {addon}"
1719        );
1720        assert!(
1721            addon.contains("#include <string.h>"),
1722            "generated addon should include string.h: {addon}"
1723        );
1724        assert!(
1725            addon.contains("#include <stdlib.h>"),
1726            "generated addon should include stdlib.h: {addon}"
1727        );
1728        assert!(
1729            addon.contains("weaveffi_error_clear(&err)"),
1730            "generated addon should clear errors: {addon}"
1731        );
1732    }
1733
1734    #[test]
1735    fn node_addon_checks_error() {
1736        let api = make_api(vec![{
1737            let mut m = make_module("math");
1738            m.functions.push(Function {
1739                name: "add".into(),
1740                params: vec![
1741                    Param {
1742                        name: "a".into(),
1743                        ty: TypeRef::I32,
1744                        mutable: false,
1745                    },
1746                    Param {
1747                        name: "b".into(),
1748                        ty: TypeRef::I32,
1749                        mutable: false,
1750                    },
1751                ],
1752                returns: Some(TypeRef::I32),
1753                doc: None,
1754                r#async: false,
1755                cancellable: false,
1756                deprecated: None,
1757                since: None,
1758            });
1759            m
1760        }]);
1761        let addon = render_addon_c(&api, true);
1762        assert!(
1763            addon.contains("err.code"),
1764            "generated addon.c should check err.code: {addon}"
1765        );
1766    }
1767
1768    #[test]
1769    fn node_strip_module_prefix() {
1770        let api = make_api(vec![{
1771            let mut m = make_module("contacts");
1772            m.functions.push(Function {
1773                name: "create_contact".into(),
1774                params: vec![Param {
1775                    name: "name".into(),
1776                    ty: TypeRef::StringUtf8,
1777                    mutable: false,
1778                }],
1779                returns: Some(TypeRef::I32),
1780                doc: None,
1781                r#async: false,
1782                cancellable: false,
1783                deprecated: None,
1784                since: None,
1785            });
1786            m
1787        }]);
1788
1789        let config = GeneratorConfig {
1790            strip_module_prefix: true,
1791            ..Default::default()
1792        };
1793
1794        let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
1795        let _ = std::fs::remove_dir_all(&tmp);
1796        std::fs::create_dir_all(&tmp).unwrap();
1797        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
1798
1799        NodeGenerator
1800            .generate_with_config(&api, out_dir, &config)
1801            .unwrap();
1802
1803        let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
1804        assert!(
1805            dts.contains("export function create_contact("),
1806            "stripped name should be create_contact: {dts}"
1807        );
1808        assert!(
1809            !dts.contains("export function contacts_create_contact("),
1810            "should not contain module-prefixed name: {dts}"
1811        );
1812
1813        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
1814        assert!(
1815            addon.contains("\"create_contact\""),
1816            "JS export name should be stripped: {addon}"
1817        );
1818        assert!(
1819            addon.contains("weaveffi_contacts_create_contact"),
1820            "C ABI call should still use full name: {addon}"
1821        );
1822
1823        let no_strip = GeneratorConfig::default();
1824        let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
1825        let _ = std::fs::remove_dir_all(&tmp2);
1826        std::fs::create_dir_all(&tmp2).unwrap();
1827        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
1828
1829        NodeGenerator
1830            .generate_with_config(&api, out_dir2, &no_strip)
1831            .unwrap();
1832
1833        let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
1834        assert!(
1835            dts2.contains("export function contacts_create_contact("),
1836            "default should use module-prefixed name: {dts2}"
1837        );
1838
1839        let _ = std::fs::remove_dir_all(&tmp);
1840        let _ = std::fs::remove_dir_all(&tmp2);
1841    }
1842
1843    #[test]
1844    fn node_typed_handle_type() {
1845        let api = make_api(vec![{
1846            let mut m = make_module("contacts");
1847            m.structs.push(StructDef {
1848                name: "Contact".into(),
1849                doc: None,
1850                fields: vec![StructField {
1851                    name: "name".into(),
1852                    ty: TypeRef::StringUtf8,
1853                    doc: None,
1854                    default: None,
1855                }],
1856                builder: false,
1857            });
1858            m.functions.push(Function {
1859                name: "get_info".into(),
1860                params: vec![Param {
1861                    name: "contact".into(),
1862                    ty: TypeRef::TypedHandle("Contact".into()),
1863                    mutable: false,
1864                }],
1865                returns: None,
1866                doc: None,
1867                r#async: false,
1868                cancellable: false,
1869                deprecated: None,
1870                since: None,
1871            });
1872            m
1873        }]);
1874        let dts = render_node_dts(&api, true);
1875        assert!(
1876            dts.contains("contact: Contact"),
1877            "TypedHandle should use class type not bigint: {dts}"
1878        );
1879    }
1880
1881    #[test]
1882    fn node_deeply_nested_optional() {
1883        let api = make_api(vec![Module {
1884            name: "edge".into(),
1885            functions: vec![Function {
1886                name: "process".into(),
1887                params: vec![Param {
1888                    name: "data".into(),
1889                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
1890                        Box::new(TypeRef::Struct("Contact".into())),
1891                    ))))),
1892                    mutable: false,
1893                }],
1894                returns: None,
1895                doc: None,
1896                r#async: false,
1897                cancellable: false,
1898                deprecated: None,
1899                since: None,
1900            }],
1901            structs: vec![StructDef {
1902                name: "Contact".into(),
1903                doc: None,
1904                fields: vec![StructField {
1905                    name: "name".into(),
1906                    ty: TypeRef::StringUtf8,
1907                    doc: None,
1908                    default: None,
1909                }],
1910                builder: false,
1911            }],
1912            enums: vec![],
1913            callbacks: vec![],
1914            listeners: vec![],
1915            errors: None,
1916            modules: vec![],
1917        }]);
1918        let dts = render_node_dts(&api, true);
1919        assert!(
1920            dts.contains("(Contact | null)[] | null"),
1921            "should contain deeply nested optional type: {dts}"
1922        );
1923    }
1924
1925    #[test]
1926    fn node_map_of_lists() {
1927        let api = make_api(vec![Module {
1928            name: "edge".into(),
1929            functions: vec![Function {
1930                name: "process".into(),
1931                params: vec![Param {
1932                    name: "scores".into(),
1933                    ty: TypeRef::Map(
1934                        Box::new(TypeRef::StringUtf8),
1935                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
1936                    ),
1937                    mutable: false,
1938                }],
1939                returns: None,
1940                doc: None,
1941                r#async: false,
1942                cancellable: false,
1943                deprecated: None,
1944                since: None,
1945            }],
1946            structs: vec![],
1947            enums: vec![],
1948            callbacks: vec![],
1949            listeners: vec![],
1950            errors: None,
1951            modules: vec![],
1952        }]);
1953        let dts = render_node_dts(&api, true);
1954        assert!(
1955            dts.contains("Record<string, number[]>"),
1956            "should contain map of lists type: {dts}"
1957        );
1958    }
1959
1960    #[test]
1961    fn node_enum_keyed_map() {
1962        let api = make_api(vec![Module {
1963            name: "edge".into(),
1964            functions: vec![Function {
1965                name: "process".into(),
1966                params: vec![Param {
1967                    name: "contacts".into(),
1968                    ty: TypeRef::Map(
1969                        Box::new(TypeRef::Enum("Color".into())),
1970                        Box::new(TypeRef::Struct("Contact".into())),
1971                    ),
1972                    mutable: false,
1973                }],
1974                returns: None,
1975                doc: None,
1976                r#async: false,
1977                cancellable: false,
1978                deprecated: None,
1979                since: None,
1980            }],
1981            structs: vec![StructDef {
1982                name: "Contact".into(),
1983                doc: None,
1984                fields: vec![StructField {
1985                    name: "name".into(),
1986                    ty: TypeRef::StringUtf8,
1987                    doc: None,
1988                    default: None,
1989                }],
1990                builder: false,
1991            }],
1992            enums: vec![EnumDef {
1993                name: "Color".into(),
1994                doc: None,
1995                variants: vec![
1996                    EnumVariant {
1997                        name: "Red".into(),
1998                        value: 0,
1999                        doc: None,
2000                    },
2001                    EnumVariant {
2002                        name: "Green".into(),
2003                        value: 1,
2004                        doc: None,
2005                    },
2006                    EnumVariant {
2007                        name: "Blue".into(),
2008                        value: 2,
2009                        doc: None,
2010                    },
2011                ],
2012            }],
2013            callbacks: vec![],
2014            listeners: vec![],
2015            errors: None,
2016            modules: vec![],
2017        }]);
2018        let dts = render_node_dts(&api, true);
2019        assert!(
2020            dts.contains("Record<Color, Contact>"),
2021            "should contain enum-keyed map type: {dts}"
2022        );
2023    }
2024
2025    #[test]
2026    fn node_no_double_free_on_error() {
2027        let api = make_api(vec![{
2028            let mut m = make_module("contacts");
2029            m.structs.push(StructDef {
2030                name: "Contact".into(),
2031                doc: None,
2032                fields: vec![StructField {
2033                    name: "name".into(),
2034                    ty: TypeRef::StringUtf8,
2035                    doc: None,
2036                    default: None,
2037                }],
2038                builder: false,
2039            });
2040            m.functions.push(Function {
2041                name: "find_contact".into(),
2042                params: vec![Param {
2043                    name: "name".into(),
2044                    ty: TypeRef::StringUtf8,
2045                    mutable: false,
2046                }],
2047                returns: Some(TypeRef::Struct("Contact".into())),
2048                doc: None,
2049                r#async: false,
2050                cancellable: false,
2051                deprecated: None,
2052                since: None,
2053            });
2054            m
2055        }]);
2056        let addon = render_addon_c(&api, true);
2057        assert!(
2058            addon.contains("free(name)"),
2059            "malloc'd JS string copy should be freed after the C call: {addon}"
2060        );
2061        assert!(
2062            !addon.contains("weaveffi_free_string(name)"),
2063            "input string param must not use weaveffi_free_string: {addon}"
2064        );
2065        let free_pos = addon
2066            .find("free(name)")
2067            .expect("free(name) should be present");
2068        let err_pos = addon
2069            .find("if (err.code != 0)")
2070            .expect("err.code check should be present");
2071        assert!(
2072            free_pos < err_pos,
2073            "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
2074        );
2075        let err_block_start = addon
2076            .find("  if (err.code != 0) {\n")
2077            .expect("error if block should be present");
2078        let after_err = &addon[err_block_start..];
2079        let err_block_end_rel = after_err
2080            .find("  }\n  napi_value ret;")
2081            .expect("napi_value ret should follow error block");
2082        let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
2083        assert!(
2084            !err_block.contains("result"),
2085            "error path should not touch result before return NULL: {err_block}"
2086        );
2087    }
2088
2089    #[test]
2090    fn node_null_check_on_optional_return() {
2091        let api = make_api(vec![{
2092            let mut m = make_module("contacts");
2093            m.structs.push(StructDef {
2094                name: "Contact".into(),
2095                doc: None,
2096                fields: vec![StructField {
2097                    name: "name".into(),
2098                    ty: TypeRef::StringUtf8,
2099                    doc: None,
2100                    default: None,
2101                }],
2102                builder: false,
2103            });
2104            m.functions.push(Function {
2105                name: "find_contact".into(),
2106                params: vec![Param {
2107                    name: "id".into(),
2108                    ty: TypeRef::I32,
2109                    mutable: false,
2110                }],
2111                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2112                    "Contact".into(),
2113                )))),
2114                doc: None,
2115                r#async: false,
2116                cancellable: false,
2117                deprecated: None,
2118                since: None,
2119            });
2120            m
2121        }]);
2122        let addon = render_addon_c(&api, true);
2123        assert!(
2124            addon.contains("if (result == NULL)"),
2125            "optional struct return should null-check before wrapping: {addon}"
2126        );
2127        assert!(
2128            addon.contains("napi_get_null"),
2129            "optional absent should return JS null via napi_get_null: {addon}"
2130        );
2131    }
2132
2133    #[test]
2134    fn node_async_returns_promise() {
2135        let api = make_api(vec![{
2136            let mut m = make_module("tasks");
2137            m.functions.push(Function {
2138                name: "run".into(),
2139                params: vec![Param {
2140                    name: "id".into(),
2141                    ty: TypeRef::I32,
2142                    mutable: false,
2143                }],
2144                returns: Some(TypeRef::StringUtf8),
2145                doc: None,
2146                r#async: true,
2147                cancellable: false,
2148                deprecated: None,
2149                since: None,
2150            });
2151            m.functions.push(Function {
2152                name: "fire_and_forget".into(),
2153                params: vec![],
2154                returns: None,
2155                doc: None,
2156                r#async: true,
2157                cancellable: false,
2158                deprecated: None,
2159                since: None,
2160            });
2161            m
2162        }]);
2163        let dts = render_node_dts(&api, true);
2164        assert!(
2165            dts.contains("Promise<"),
2166            "async function should return Promise in .d.ts: {dts}"
2167        );
2168        assert!(
2169            dts.contains("): Promise<string>"),
2170            "async string return should be Promise<string>: {dts}"
2171        );
2172        assert!(
2173            dts.contains("): Promise<void>"),
2174            "async void return should be Promise<void>: {dts}"
2175        );
2176    }
2177
2178    #[test]
2179    fn node_addon_creates_promise() {
2180        let api = make_api(vec![{
2181            let mut m = make_module("tasks");
2182            m.functions.push(Function {
2183                name: "run".into(),
2184                params: vec![Param {
2185                    name: "id".into(),
2186                    ty: TypeRef::I32,
2187                    mutable: false,
2188                }],
2189                returns: Some(TypeRef::I32),
2190                doc: None,
2191                r#async: true,
2192                cancellable: false,
2193                deprecated: None,
2194                since: None,
2195            });
2196            m
2197        }]);
2198        let addon = render_addon_c(&api, true);
2199        assert!(
2200            addon.contains("napi_create_promise"),
2201            "async addon should call napi_create_promise: {addon}"
2202        );
2203        assert!(
2204            addon.contains("napi_resolve_deferred"),
2205            "async callback should call napi_resolve_deferred: {addon}"
2206        );
2207        assert!(
2208            addon.contains("napi_reject_deferred"),
2209            "async callback should call napi_reject_deferred: {addon}"
2210        );
2211        assert!(
2212            addon.contains("weaveffi_napi_async_ctx"),
2213            "async addon should define async context struct: {addon}"
2214        );
2215        assert!(
2216            addon.contains("weaveffi_tasks_run_async("),
2217            "async addon should call the _async C function: {addon}"
2218        );
2219        assert!(
2220            addon.contains("weaveffi_tasks_run_napi_cb"),
2221            "async addon should define the callback: {addon}"
2222        );
2223    }
2224}