Skip to main content

weaveffi_gen_node/
lib.rs

1//! Node.js (N-API) binding generator for WeaveFFI.
2//!
3//! Emits a JavaScript loader plus TypeScript type definitions for the
4//! companion N-API addon. Async functions surface as `Promise`-returning
5//! methods. Implements the [`Generator`] trait.
6
7use std::collections::HashMap;
8
9use anyhow::Result;
10use camino::Utf8Path;
11use heck::ToUpperCamelCase;
12use serde::{Deserialize, Serialize};
13use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle};
14use weaveffi_core::codegen::Generator;
15use weaveffi_core::model::{BindingModel, FnBinding, ParamBinding, StructBinding};
16use weaveffi_core::utils::{
17    c_abi_struct_name, local_type_name, render_json_prelude, render_prelude, render_trailer,
18    wrapper_name, CommentStyle,
19};
20use weaveffi_ir::ir::{Api, TypeRef};
21
22/// Per-target configuration for [`NodeGenerator`].
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24#[serde(default)]
25pub struct NodeConfig {
26    /// npm package name (default `"weaveffi"`).
27    pub package_name: Option<String>,
28    /// When `true`, strip the IR module name prefix from emitted
29    /// JS/TS function names.
30    pub strip_module_prefix: bool,
31    /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally
32    /// via `[global] c_prefix`; honored so the native addon calls the same
33    /// exported symbols the producer emits.
34    pub prefix: Option<String>,
35    /// Basename of the IDL the CLI was invoked with.
36    #[serde(skip)]
37    pub input_basename: Option<String>,
38}
39
40impl NodeConfig {
41    pub fn package_name(&self) -> &str {
42        self.package_name.as_deref().unwrap_or("weaveffi")
43    }
44
45    pub fn prefix(&self) -> &str {
46        self.prefix.as_deref().unwrap_or("weaveffi")
47    }
48
49    pub fn input_basename(&self) -> &str {
50        self.input_basename.as_deref().unwrap_or("weaveffi.yml")
51    }
52}
53
54pub struct NodeGenerator;
55
56impl NodeGenerator {
57    fn generate_impl(
58        &self,
59        api: &Api,
60        out_dir: &Utf8Path,
61        package_name: &str,
62        prefix: &str,
63        strip_module_prefix: bool,
64        input_basename: &str,
65    ) -> Result<()> {
66        let dir = out_dir.join("node");
67        std::fs::create_dir_all(&dir)?;
68        let dbl = CommentStyle::DoubleSlash;
69        std::fs::write(
70            dir.join("index.js"),
71            format!(
72                "{}module.exports = require('./index.node')\n\n{}",
73                render_prelude(dbl, input_basename),
74                render_trailer(dbl, "index.js"),
75            ),
76        )?;
77        std::fs::write(
78            dir.join("types.d.ts"),
79            render_node_dts(api, prefix, strip_module_prefix, input_basename),
80        )?;
81        std::fs::write(
82            dir.join("package.json"),
83            render_package_json(package_name, input_basename),
84        )?;
85        std::fs::write(dir.join("binding.gyp"), render_binding_gyp(input_basename))?;
86        std::fs::write(
87            dir.join("weaveffi_addon.c"),
88            render_addon_c(api, prefix, strip_module_prefix, input_basename),
89        )?;
90        Ok(())
91    }
92}
93
94impl Generator for NodeGenerator {
95    type Config = NodeConfig;
96
97    fn name(&self) -> &'static str {
98        "node"
99    }
100
101    fn generate(&self, api: &Api, out_dir: &Utf8Path, config: &Self::Config) -> Result<()> {
102        self.generate_impl(
103            api,
104            out_dir,
105            config.package_name(),
106            config.prefix(),
107            config.strip_module_prefix,
108            config.input_basename(),
109        )
110    }
111
112    fn output_files(&self, _api: &Api, out_dir: &Utf8Path, _config: &Self::Config) -> Vec<String> {
113        let mut files = vec![
114            out_dir.join("node/binding.gyp").to_string(),
115            out_dir.join("node/index.js").to_string(),
116            out_dir.join("node/package.json").to_string(),
117            out_dir.join("node/types.d.ts").to_string(),
118            out_dir.join("node/weaveffi_addon.c").to_string(),
119        ];
120        files.sort();
121        files
122    }
123}
124
125fn render_package_json(name: &str, input_basename: &str) -> String {
126    let prelude = render_json_prelude(input_basename);
127    format!(
128        "{{\n{prelude}  \"name\": \"{name}\",\n  \"version\": \"0.1.0\",\n  \"main\": \"index.js\",\n  \"types\": \"types.d.ts\",\n  \"gypfile\": true,\n  \"scripts\": {{\n    \"install\": \"node-gyp rebuild\"\n  }}\n}}\n"
129    )
130}
131
132fn render_binding_gyp(input_basename: &str) -> String {
133    let prelude = render_prelude(CommentStyle::Hash, input_basename);
134    let trailer = render_trailer(CommentStyle::Hash, "binding.gyp");
135    format!(
136        "{prelude}{{\n  \"targets\": [\n    {{\n      \"target_name\": \"weaveffi\",\n      \"sources\": [\"weaveffi_addon.c\"],\n      \"include_dirs\": [\"../c\"],\n      \"libraries\": [\"-lweaveffi\"]\n    }}\n  ]\n}}\n\n{trailer}"
137    )
138}
139
140fn is_c_ptr_type(ty: &TypeRef) -> bool {
141    matches!(
142        ty,
143        TypeRef::StringUtf8
144            | TypeRef::Bytes
145            | TypeRef::Struct(_)
146            | TypeRef::List(_)
147            | TypeRef::Map(_, _)
148            | TypeRef::Iterator(_)
149    )
150}
151
152fn c_elem_type(ty: &TypeRef, module: &str, prefix: &str) -> String {
153    match ty {
154        TypeRef::I32 => "int32_t".into(),
155        TypeRef::U32 => "uint32_t".into(),
156        TypeRef::I64 => "int64_t".into(),
157        TypeRef::F64 => "double".into(),
158        TypeRef::Bool => "bool".into(),
159        TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
160        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
161        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
162        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
163        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
164        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
165            c_elem_type(inner, module, prefix)
166        }
167        TypeRef::Map(_, _) => "void*".into(),
168    }
169}
170
171fn c_ret_type_str(ty: &TypeRef, module: &str, prefix: &str) -> String {
172    match ty {
173        TypeRef::I32 => "int32_t".into(),
174        TypeRef::U32 => "uint32_t".into(),
175        TypeRef::I64 => "int64_t".into(),
176        TypeRef::F64 => "double".into(),
177        TypeRef::Bool => "bool".into(),
178        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
179        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
180        TypeRef::TypedHandle(_) | TypeRef::Handle => "weaveffi_handle_t".into(),
181        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
182        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
183        TypeRef::Optional(inner) => {
184            if is_c_ptr_type(inner) {
185                c_ret_type_str(inner, module, prefix)
186            } else {
187                format!("{}*", c_elem_type(inner, module, prefix))
188            }
189        }
190        TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module, prefix)),
191        TypeRef::Map(_, _) => "void".into(),
192        TypeRef::Iterator(_) => "void*".into(),
193    }
194}
195
196fn napi_getter(ty: &TypeRef) -> &'static str {
197    match ty {
198        TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
199        TypeRef::U32 => "napi_get_value_uint32",
200        TypeRef::I64 | TypeRef::Handle | TypeRef::TypedHandle(_) | TypeRef::Struct(_) => {
201            "napi_get_value_int64"
202        }
203        TypeRef::F64 => "napi_get_value_double",
204        TypeRef::Bool => "napi_get_value_bool",
205        _ => "napi_get_value_int64",
206    }
207}
208
209fn render_addon_c(
210    api: &Api,
211    prefix: &str,
212    strip_module_prefix: bool,
213    input_basename: &str,
214) -> String {
215    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
216    out.push_str(&format!(
217        "#include <node_api.h>\n#include \"{prefix}.h\"\n#include <stdlib.h>\n#include <string.h>\n\n"
218    ));
219
220    let model = BindingModel::build(api, prefix);
221    let has_async = model.functions().any(|(_, f)| f.is_async);
222    if has_async {
223        out.push_str("typedef struct {\n");
224        out.push_str("    napi_env env;\n");
225        out.push_str("    napi_deferred deferred;\n");
226        out.push_str("} weaveffi_napi_async_ctx;\n\n");
227    }
228
229    let mut all_exports: Vec<(String, String)> = Vec::new();
230    let structs = struct_registry(&model);
231
232    for m in &model.modules {
233        for f in &m.functions {
234            let c_name = &f.c_base;
235            let napi_name = format!("Napi_{c_name}");
236            let js_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
237            all_exports.push((js_name, napi_name.clone()));
238
239            if f.is_async {
240                render_async_callback(&mut out, f, c_name, &m.path, prefix, &structs);
241            }
242
243            out.push_str(&format!(
244                "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
245            ));
246            if f.is_async {
247                render_async_napi_body(&mut out, f, c_name, &m.path, prefix);
248            } else {
249                render_napi_body(&mut out, f, c_name, &m.path, prefix, &structs);
250            }
251            out.push_str("}\n\n");
252        }
253    }
254
255    out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
256    if !all_exports.is_empty() {
257        out.push_str("  napi_property_descriptor props[] = {\n");
258        for (js_name, napi_fn) in &all_exports {
259            out.push_str(&format!(
260                "    {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
261            ));
262        }
263        out.push_str("  };\n");
264        out.push_str(&format!(
265            "  napi_define_properties(env, exports, {}, props);\n",
266            all_exports.len()
267        ));
268    }
269    out.push_str("  return exports;\n");
270    out.push_str("}\n\n");
271    out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n");
272    out.push_str(&render_trailer(
273        CommentStyle::DoubleSlash,
274        "weaveffi_addon.c",
275    ));
276    out
277}
278
279fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str, prefix: &str) -> String {
280    match ret {
281        None => String::new(),
282        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
283        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
284            ", const uint8_t* result, size_t result_len".into()
285        }
286        Some(TypeRef::List(inner)) => {
287            let et = c_elem_type(inner, module, prefix);
288            format!(", {et}* result, size_t result_len")
289        }
290        Some(TypeRef::Map(k, v)) => {
291            let kt = c_elem_type(k, module, prefix);
292            let vt = c_elem_type(v, module, prefix);
293            format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
294        }
295        Some(t) => format!(", {} result", c_ret_type_str(t, module, prefix)),
296    }
297}
298
299fn emit_async_resolve_value(
300    out: &mut String,
301    ret: Option<&TypeRef>,
302    module: &str,
303    prefix: &str,
304    structs: &HashMap<String, StructBinding>,
305) {
306    out.push_str("        napi_value val;\n");
307    match ret {
308        None => out.push_str("        napi_get_undefined(ctx->env, &val);\n"),
309        Some(TypeRef::I32) => out.push_str("        napi_create_int32(ctx->env, result, &val);\n"),
310        Some(TypeRef::U32) => out.push_str("        napi_create_uint32(ctx->env, result, &val);\n"),
311        Some(TypeRef::I64) => out.push_str("        napi_create_int64(ctx->env, result, &val);\n"),
312        Some(TypeRef::F64) => out.push_str("        napi_create_double(ctx->env, result, &val);\n"),
313        Some(TypeRef::Bool) => out.push_str("        napi_get_boolean(ctx->env, result, &val);\n"),
314        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
315            out.push_str(
316                "        napi_create_string_utf8(ctx->env, result, NAPI_AUTO_LENGTH, &val);\n",
317            );
318        }
319        Some(TypeRef::TypedHandle(_) | TypeRef::Handle) => {
320            out.push_str("        napi_create_int64(ctx->env, (int64_t)result, &val);\n");
321        }
322        Some(TypeRef::Struct(name)) => {
323            emit_struct_to_object(
324                out, "ctx->env", name, "result", "val", module, prefix, structs, "        ", true,
325            );
326        }
327        Some(TypeRef::Enum(_)) => {
328            out.push_str("        napi_create_int32(ctx->env, (int32_t)result, &val);\n");
329        }
330        Some(TypeRef::Iterator(_)) => {
331            out.push_str("        napi_create_int64(ctx->env, (int64_t)(intptr_t)result, &val);\n");
332        }
333        _ => out.push_str("        napi_get_undefined(ctx->env, &val);\n"),
334    }
335    out.push_str("        napi_resolve_deferred(ctx->env, ctx->deferred, val);\n");
336}
337
338fn render_async_callback(
339    out: &mut String,
340    f: &FnBinding,
341    c_name: &str,
342    module: &str,
343    prefix: &str,
344    structs: &HashMap<String, StructBinding>,
345) {
346    let cb_name = format!("{c_name}_napi_cb");
347    let cb_result = async_cb_result_params_node(f.ret.as_ref(), module, prefix);
348
349    out.push_str(&format!(
350        "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
351    ));
352    out.push_str("    weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)context;\n");
353    out.push_str("    if (err != NULL && err->code != 0) {\n");
354    out.push_str("        napi_value err_msg;\n");
355    out.push_str(
356        "        napi_create_string_utf8(ctx->env, err->message, NAPI_AUTO_LENGTH, &err_msg);\n",
357    );
358    out.push_str("        napi_reject_deferred(ctx->env, ctx->deferred, err_msg);\n");
359    out.push_str("    } else {\n");
360    emit_async_resolve_value(out, f.ret.as_ref(), module, prefix, structs);
361    out.push_str("    }\n");
362    out.push_str("    free(ctx);\n");
363    out.push_str("}\n\n");
364}
365
366fn render_async_napi_body(
367    out: &mut String,
368    f: &FnBinding,
369    c_name: &str,
370    module: &str,
371    prefix: &str,
372) {
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(
387            out,
388            &mut c_args,
389            &mut cleanups,
390            &p.ty,
391            &p.name,
392            i,
393            module,
394            prefix,
395        );
396    }
397
398    out.push_str(
399        "  weaveffi_napi_async_ctx* ctx = (weaveffi_napi_async_ctx*)malloc(sizeof(weaveffi_napi_async_ctx));\n",
400    );
401    out.push_str("  ctx->env = env;\n");
402    out.push_str("  napi_value promise;\n");
403    out.push_str("  napi_create_promise(env, &ctx->deferred, &promise);\n");
404
405    if f.cancellable {
406        c_args.push("NULL".into());
407    }
408
409    let cb_name = format!("{c_name}_napi_cb");
410    c_args.push(cb_name);
411    c_args.push("ctx".into());
412    let args_str = c_args.join(", ");
413    out.push_str(&format!("  {c_name}_async({args_str});\n"));
414
415    for cleanup in &cleanups {
416        out.push_str(cleanup);
417    }
418
419    out.push_str("  return promise;\n");
420}
421
422fn render_napi_body(
423    out: &mut String,
424    f: &FnBinding,
425    c_name: &str,
426    module: &str,
427    prefix: &str,
428    structs: &HashMap<String, StructBinding>,
429) {
430    let n = f.params.len();
431    if n > 0 {
432        out.push_str(&format!("  size_t argc = {n};\n"));
433        out.push_str(&format!("  napi_value args[{n}];\n"));
434        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
435    } else {
436        out.push_str("  size_t argc = 0;\n");
437        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
438    }
439
440    let mut c_args: Vec<String> = Vec::new();
441    let mut cleanups: Vec<String> = Vec::new();
442    for (i, p) in f.params.iter().enumerate() {
443        emit_param(
444            out,
445            &mut c_args,
446            &mut cleanups,
447            &p.ty,
448            &p.name,
449            i,
450            module,
451            prefix,
452        );
453    }
454
455    out.push_str("  weaveffi_error err = {0};\n");
456
457    if let Some(ret) = &f.ret {
458        emit_ret_out_params(out, &mut c_args, ret, module, prefix);
459    }
460    c_args.push("&err".to_string());
461
462    let args_str = c_args.join(", ");
463    let ret_type = f.ret.as_ref().map(|r| c_ret_type_str(r, module, prefix));
464    match &ret_type {
465        Some(rt) if rt != "void" => {
466            out.push_str(&format!("  {rt} result = {c_name}({args_str});\n"));
467        }
468        _ => {
469            out.push_str(&format!("  {c_name}({args_str});\n"));
470        }
471    }
472
473    for cleanup in &cleanups {
474        out.push_str(cleanup);
475    }
476
477    out.push_str("  if (err.code != 0) {\n");
478    out.push_str("    napi_throw_error(env, NULL, err.message);\n");
479    out.push_str("    weaveffi_error_clear(&err);\n");
480    out.push_str("    return NULL;\n");
481    out.push_str("  }\n");
482
483    match &f.ret {
484        Some(ret) => emit_ret_to_napi(out, ret, module, prefix, &f.name, structs),
485        None => {
486            out.push_str("  napi_value ret;\n");
487            out.push_str("  napi_get_undefined(env, &ret);\n");
488            out.push_str("  return ret;\n");
489        }
490    }
491}
492
493#[allow(clippy::too_many_arguments)]
494fn emit_param(
495    out: &mut String,
496    c_args: &mut Vec<String>,
497    cleanups: &mut Vec<String>,
498    ty: &TypeRef,
499    name: &str,
500    idx: usize,
501    module: &str,
502    prefix: &str,
503) {
504    match ty {
505        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
506            let ct = c_elem_type(ty, module, prefix);
507            let getter = napi_getter(ty);
508            out.push_str(&format!("  {ct} {name};\n"));
509            out.push_str(&format!("  {getter}(env, args[{idx}], &{name});\n"));
510            c_args.push(name.into());
511        }
512        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
513            out.push_str(&format!("  size_t {name}_len;\n"));
514            out.push_str(&format!(
515                "  napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
516            ));
517            out.push_str(&format!(
518                "  char* {name} = (char*)malloc({name}_len + 1);\n"
519            ));
520            out.push_str(&format!(
521                "  napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
522            ));
523            c_args.push(name.into());
524            cleanups.push(format!("  free({name});\n"));
525        }
526        TypeRef::TypedHandle(_) | TypeRef::Handle => {
527            out.push_str(&format!("  int64_t {name}_raw;\n"));
528            out.push_str(&format!(
529                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
530            ));
531            c_args.push(format!("(weaveffi_handle_t){name}_raw"));
532        }
533        TypeRef::Enum(e) => {
534            out.push_str(&format!("  int32_t {name};\n"));
535            out.push_str(&format!(
536                "  napi_get_value_int32(env, args[{idx}], &{name});\n"
537            ));
538            c_args.push(format!("({prefix}_{module}_{e}){name}"));
539        }
540        TypeRef::Struct(s) => {
541            let abi = c_abi_struct_name(s, module, prefix);
542            out.push_str(&format!("  int64_t {name}_raw;\n"));
543            out.push_str(&format!(
544                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
545            ));
546            c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
547        }
548        TypeRef::Optional(inner) => {
549            out.push_str(&format!("  napi_valuetype {name}_type;\n"));
550            out.push_str(&format!("  napi_typeof(env, args[{idx}], &{name}_type);\n"));
551            emit_optional_param(out, c_args, cleanups, inner, name, idx, module, prefix);
552        }
553        TypeRef::List(inner) => {
554            emit_list_param(out, c_args, cleanups, inner, name, idx, module, prefix);
555        }
556        TypeRef::Bytes | TypeRef::BorrowedBytes => {
557            out.push_str(&format!("  void* {name}_raw;\n"));
558            out.push_str(&format!("  size_t {name}_len;\n"));
559            out.push_str(&format!(
560                "  napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
561            ));
562            c_args.push(format!("(const uint8_t*){name}_raw"));
563            c_args.push(format!("{name}_len"));
564        }
565        TypeRef::Map(k, v) => {
566            emit_map_param(out, c_args, cleanups, k, v, name, idx, module, prefix);
567        }
568        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
569    }
570}
571
572fn emit_opt_val(
573    out: &mut String,
574    c_args: &mut Vec<String>,
575    c_type: &str,
576    napi_fn: &str,
577    name: &str,
578    idx: usize,
579) {
580    out.push_str(&format!("  {c_type} {name}_val;\n"));
581    out.push_str(&format!("  const {c_type}* {name}_ptr = NULL;\n"));
582    out.push_str(&format!(
583        "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
584    ));
585    out.push_str(&format!("    {napi_fn}(env, args[{idx}], &{name}_val);\n"));
586    out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
587    out.push_str("  }\n");
588    c_args.push(format!("{name}_ptr"));
589}
590
591#[allow(clippy::too_many_arguments)]
592fn emit_optional_param(
593    out: &mut String,
594    c_args: &mut Vec<String>,
595    cleanups: &mut Vec<String>,
596    inner: &TypeRef,
597    name: &str,
598    idx: usize,
599    module: &str,
600    prefix: &str,
601) {
602    match inner {
603        TypeRef::I32 => {
604            emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
605        }
606        TypeRef::U32 => {
607            emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
608        }
609        TypeRef::I64 => {
610            emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
611        }
612        TypeRef::F64 => {
613            emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
614        }
615        TypeRef::Bool => {
616            emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
617        }
618        TypeRef::TypedHandle(_) | TypeRef::Handle => {
619            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
620            out.push_str(&format!("  weaveffi_handle_t {name}_val;\n"));
621            out.push_str(&format!("  const weaveffi_handle_t* {name}_ptr = NULL;\n"));
622            out.push_str(&format!(
623                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
624            ));
625            out.push_str(&format!(
626                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
627            ));
628            out.push_str(&format!(
629                "    {name}_val = (weaveffi_handle_t){name}_raw;\n"
630            ));
631            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
632            out.push_str("  }\n");
633            c_args.push(format!("{name}_ptr"));
634        }
635        TypeRef::Enum(e) => {
636            let etype = format!("{prefix}_{module}_{e}");
637            out.push_str(&format!("  int32_t {name}_raw;\n"));
638            out.push_str(&format!("  {etype} {name}_val;\n"));
639            out.push_str(&format!("  const {etype}* {name}_ptr = NULL;\n"));
640            out.push_str(&format!(
641                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
642            ));
643            out.push_str(&format!(
644                "    napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
645            ));
646            out.push_str(&format!("    {name}_val = ({etype}){name}_raw;\n"));
647            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
648            out.push_str("  }\n");
649            c_args.push(format!("{name}_ptr"));
650        }
651        TypeRef::StringUtf8 => {
652            out.push_str(&format!("  char* {name} = NULL;\n"));
653            out.push_str(&format!(
654                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
655            ));
656            out.push_str(&format!("    size_t {name}_len;\n"));
657            out.push_str(&format!(
658                "    napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
659            ));
660            out.push_str(&format!("    {name} = (char*)malloc({name}_len + 1);\n"));
661            out.push_str(&format!(
662                "    napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
663            ));
664            out.push_str("  }\n");
665            c_args.push(name.into());
666            cleanups.push(format!("  free({name});\n"));
667        }
668        TypeRef::Struct(s) => {
669            let abi = c_abi_struct_name(s, module, prefix);
670            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
671            out.push_str(&format!(
672                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
673            ));
674            out.push_str(&format!(
675                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
676            ));
677            out.push_str("  }\n");
678            c_args.push(format!(
679                "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
680            ));
681        }
682        _ => {
683            emit_param(out, c_args, cleanups, inner, name, idx, module, prefix);
684        }
685    }
686}
687
688#[allow(clippy::too_many_arguments)]
689fn emit_list_param(
690    out: &mut String,
691    c_args: &mut Vec<String>,
692    cleanups: &mut Vec<String>,
693    inner: &TypeRef,
694    name: &str,
695    idx: usize,
696    module: &str,
697    prefix: &str,
698) {
699    let et = c_elem_type(inner, module, prefix);
700    out.push_str(&format!("  uint32_t {name}_count;\n"));
701    out.push_str(&format!(
702        "  napi_get_array_length(env, args[{idx}], &{name}_count);\n"
703    ));
704    out.push_str(&format!(
705        "  {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
706    ));
707    out.push_str(&format!(
708        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
709    ));
710    out.push_str(&format!("    napi_value {name}_el;\n"));
711    out.push_str(&format!(
712        "    napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
713    ));
714
715    match inner {
716        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
717            let getter = napi_getter(inner);
718            out.push_str(&format!(
719                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
720            ));
721        }
722        TypeRef::TypedHandle(_) | TypeRef::Handle => {
723            out.push_str(&format!("    int64_t {name}_h;\n"));
724            out.push_str(&format!(
725                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
726            ));
727            out.push_str(&format!(
728                "    {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
729            ));
730        }
731        TypeRef::Enum(_) => {
732            out.push_str(&format!("    int32_t {name}_ev;\n"));
733            out.push_str(&format!(
734                "    napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
735            ));
736            out.push_str(&format!("    {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
737        }
738        TypeRef::StringUtf8 => {
739            out.push_str(&format!("    size_t {name}_sl;\n"));
740            out.push_str(&format!(
741                "    napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
742            ));
743            out.push_str(&format!(
744                "    char* {name}_s = (char*)malloc({name}_sl + 1);\n"
745            ));
746            out.push_str(&format!(
747                "    napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
748            ));
749            out.push_str(&format!("    {name}_arr[{name}_i] = {name}_s;\n"));
750        }
751        TypeRef::Struct(_) => {
752            out.push_str(&format!("    int64_t {name}_sp;\n"));
753            out.push_str(&format!(
754                "    napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
755            ));
756            out.push_str(&format!(
757                "    {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
758            ));
759        }
760        _ => {
761            let getter = napi_getter(inner);
762            out.push_str(&format!(
763                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
764            ));
765        }
766    }
767
768    out.push_str("  }\n");
769    c_args.push(format!("{name}_arr"));
770    c_args.push(format!("(size_t){name}_count"));
771
772    if matches!(inner, TypeRef::StringUtf8) {
773        cleanups.push(format!(
774            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
775        ));
776    }
777    cleanups.push(format!("  free({name}_arr);\n"));
778}
779
780#[allow(clippy::too_many_arguments)]
781fn emit_map_param(
782    out: &mut String,
783    c_args: &mut Vec<String>,
784    cleanups: &mut Vec<String>,
785    k: &TypeRef,
786    v: &TypeRef,
787    name: &str,
788    idx: usize,
789    module: &str,
790    prefix: &str,
791) {
792    let kt = c_elem_type(k, module, prefix);
793    let vt = c_elem_type(v, module, prefix);
794    out.push_str(&format!("  napi_value {name}_keys_napi;\n"));
795    out.push_str(&format!(
796        "  napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
797    ));
798    out.push_str(&format!("  uint32_t {name}_count;\n"));
799    out.push_str(&format!(
800        "  napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
801    ));
802    out.push_str(&format!(
803        "  {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
804    ));
805    out.push_str(&format!(
806        "  {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
807    ));
808    out.push_str(&format!(
809        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
810    ));
811    out.push_str(&format!("    napi_value {name}_k;\n"));
812    out.push_str(&format!(
813        "    napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
814    ));
815
816    if matches!(k, TypeRef::StringUtf8) {
817        out.push_str(&format!("    size_t {name}_kl;\n"));
818        out.push_str(&format!(
819            "    napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
820        ));
821        out.push_str(&format!(
822            "    char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
823        ));
824        out.push_str(&format!(
825            "    napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
826        ));
827        out.push_str(&format!("    {name}_keys[{name}_i] = {name}_ks;\n"));
828    } else {
829        out.push_str(&format!("    napi_value {name}_kn;\n"));
830        out.push_str(&format!(
831            "    napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
832        ));
833        let kgetter = napi_getter(k);
834        out.push_str(&format!(
835            "    {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
836        ));
837    }
838
839    out.push_str(&format!("    napi_value {name}_v;\n"));
840    out.push_str(&format!(
841        "    napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
842    ));
843
844    if matches!(v, TypeRef::StringUtf8) {
845        out.push_str(&format!("    size_t {name}_vl;\n"));
846        out.push_str(&format!(
847            "    napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
848        ));
849        out.push_str(&format!(
850            "    char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
851        ));
852        out.push_str(&format!(
853            "    napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
854        ));
855        out.push_str(&format!("    {name}_values[{name}_i] = {name}_vs;\n"));
856    } else {
857        let vgetter = napi_getter(v);
858        out.push_str(&format!(
859            "    {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
860        ));
861    }
862
863    out.push_str("  }\n");
864    c_args.push(format!("{name}_keys"));
865    c_args.push(format!("{name}_values"));
866    c_args.push(format!("(size_t){name}_count"));
867
868    if matches!(k, TypeRef::StringUtf8) {
869        cleanups.push(format!(
870            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
871        ));
872    }
873    cleanups.push(format!("  free({name}_keys);\n"));
874    if matches!(v, TypeRef::StringUtf8) {
875        cleanups.push(format!(
876            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
877        ));
878    }
879    cleanups.push(format!("  free({name}_values);\n"));
880}
881
882fn emit_ret_out_params(
883    out: &mut String,
884    c_args: &mut Vec<String>,
885    ty: &TypeRef,
886    module: &str,
887    prefix: &str,
888) {
889    match ty {
890        TypeRef::Bytes | TypeRef::List(_) => {
891            out.push_str("  size_t out_len;\n");
892            c_args.push("&out_len".into());
893        }
894        TypeRef::Map(k, v) => {
895            let kt = c_elem_type(k, module, prefix);
896            let vt = c_elem_type(v, module, prefix);
897            out.push_str(&format!("  {kt}* out_keys = NULL;\n"));
898            out.push_str(&format!("  {vt}* out_values = NULL;\n"));
899            out.push_str("  size_t out_len = 0;\n");
900            c_args.push("out_keys".into());
901            c_args.push("out_values".into());
902            c_args.push("&out_len".into());
903        }
904        TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
905            emit_ret_out_params(out, c_args, inner, module, prefix);
906        }
907        _ => {}
908    }
909}
910
911/// Build a `name -> StructDef` registry over every (possibly nested) module so
912/// that struct-returning functions can materialize a real JS object (matching
913/// the shape declared in `types.d.ts`) instead of leaking a raw handle number.
914fn struct_registry(model: &BindingModel) -> HashMap<String, StructBinding> {
915    model
916        .modules
917        .iter()
918        .flat_map(|m| m.structs.iter())
919        .map(|s| (s.name.clone(), s.clone()))
920        .collect()
921}
922
923/// Materialize an *owned* C struct pointer (`ptr_expr`) into a plain JS object
924/// assigned to `obj_var`, by invoking each generated field getter. The pointer
925/// is consumed: after the fields are read it is destroyed, because the C ABI
926/// hands back owned struct handles (the same ownership the other backends free).
927#[allow(clippy::too_many_arguments)]
928fn emit_struct_to_object(
929    out: &mut String,
930    env: &str,
931    struct_name: &str,
932    ptr_expr: &str,
933    obj_var: &str,
934    module: &str,
935    prefix: &str,
936    structs: &HashMap<String, StructBinding>,
937    indent: &str,
938    destroy: bool,
939) {
940    let Some(def) = structs.get(local_type_name(struct_name)).cloned() else {
941        // Unknown struct: fall back to the raw handle rather than emit broken C.
942        out.push_str(&format!(
943            "{indent}napi_create_int64({env}, (int64_t)(intptr_t){ptr_expr}, &{obj_var});\n"
944        ));
945        return;
946    };
947    let abi = &def.c_tag;
948    let p = format!("{obj_var}_p");
949    out.push_str(&format!("{indent}{{\n"));
950    out.push_str(&format!("{indent}  {abi}* {p} = ({abi}*){ptr_expr};\n"));
951    out.push_str(&format!(
952        "{indent}  napi_create_object({env}, &{obj_var});\n"
953    ));
954    for field in &def.fields {
955        let getter = &field.getter_symbol;
956        let fv = format!("{obj_var}_{}", field.name);
957        out.push_str(&format!("{indent}  napi_value {fv};\n"));
958        emit_struct_field_to_napi(
959            out,
960            env,
961            &field.ty,
962            getter,
963            &p,
964            &fv,
965            module,
966            prefix,
967            structs,
968            &format!("{indent}  "),
969        );
970        out.push_str(&format!(
971            "{indent}  napi_set_named_property({env}, {obj_var}, \"{}\", {fv});\n",
972            field.name
973        ));
974    }
975    if destroy {
976        out.push_str(&format!("{indent}  {}({p});\n", def.destroy_symbol));
977    }
978    out.push_str(&format!("{indent}}}\n"));
979}
980
981/// Marshal one struct field, read via `getter(pv)`, into the JS value `fv`.
982/// Scalars, enums, handles, owned strings, optional strings and nested structs
983/// are fully materialized; collection- and optional-scalar-typed fields fall
984/// back to `null` (the getter ABIs differ and no conformance sample needs them).
985#[allow(clippy::too_many_arguments)]
986fn emit_struct_field_to_napi(
987    out: &mut String,
988    env: &str,
989    ty: &TypeRef,
990    getter: &str,
991    pv: &str,
992    fv: &str,
993    module: &str,
994    prefix: &str,
995    structs: &HashMap<String, StructBinding>,
996    indent: &str,
997) {
998    match ty {
999        TypeRef::I32 => out.push_str(&format!(
1000            "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
1001        )),
1002        TypeRef::U32 => out.push_str(&format!(
1003            "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
1004        )),
1005        TypeRef::I64 => out.push_str(&format!(
1006            "{indent}napi_create_int64({env}, {getter}({pv}), &{fv});\n"
1007        )),
1008        TypeRef::F64 => out.push_str(&format!(
1009            "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
1010        )),
1011        TypeRef::Bool => out.push_str(&format!(
1012            "{indent}napi_get_boolean({env}, {getter}({pv}), &{fv});\n"
1013        )),
1014        TypeRef::Enum(_) => out.push_str(&format!(
1015            "{indent}napi_create_int32({env}, (int32_t){getter}({pv}), &{fv});\n"
1016        )),
1017        TypeRef::Handle | TypeRef::TypedHandle(_) => out.push_str(&format!(
1018            "{indent}napi_create_int64({env}, (int64_t){getter}({pv}), &{fv});\n"
1019        )),
1020        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1021            let owned = matches!(ty, TypeRef::StringUtf8);
1022            out.push_str(&format!("{indent}{{\n"));
1023            out.push_str(&format!(
1024                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
1025            ));
1026            out.push_str(&format!(
1027                "{indent}  napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});\n"
1028            ));
1029            if owned {
1030                out.push_str(&format!("{indent}  weaveffi_free_string({fv}_s);\n"));
1031            }
1032            out.push_str(&format!("{indent}}}\n"));
1033        }
1034        TypeRef::Struct(name) => {
1035            emit_struct_to_object(
1036                out,
1037                env,
1038                name,
1039                &format!("{getter}({pv})"),
1040                fv,
1041                module,
1042                prefix,
1043                structs,
1044                indent,
1045                true,
1046            );
1047        }
1048        TypeRef::Optional(inner)
1049            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
1050        {
1051            let owned = matches!(inner.as_ref(), TypeRef::StringUtf8);
1052            out.push_str(&format!("{indent}{{\n"));
1053            out.push_str(&format!(
1054                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
1055            ));
1056            out.push_str(&format!(
1057                "{indent}  if ({fv}_s == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1058            ));
1059            out.push_str(&format!(
1060                "{indent}  else {{ napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});"
1061            ));
1062            if owned {
1063                out.push_str(&format!(" weaveffi_free_string({fv}_s);"));
1064            }
1065            out.push_str(" }\n");
1066            out.push_str(&format!("{indent}}}\n"));
1067        }
1068        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Struct(_)) => {
1069            let TypeRef::Struct(name) = inner.as_ref() else {
1070                unreachable!()
1071            };
1072            let abi = c_abi_struct_name(name, module, prefix);
1073            out.push_str(&format!("{indent}{{\n"));
1074            out.push_str(&format!("{indent}  {abi}* {fv}_sp = {getter}({pv});\n"));
1075            out.push_str(&format!(
1076                "{indent}  if ({fv}_sp == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1077            ));
1078            out.push_str(&format!("{indent}  else {{\n"));
1079            emit_struct_to_object(
1080                out,
1081                env,
1082                name,
1083                &format!("{fv}_sp"),
1084                fv,
1085                module,
1086                prefix,
1087                structs,
1088                &format!("{indent}    "),
1089                true,
1090            );
1091            out.push_str(&format!("{indent}  }}\n"));
1092            out.push_str(&format!("{indent}}}\n"));
1093        }
1094        _ => out.push_str(&format!("{indent}napi_get_null({env}, &{fv});\n")),
1095    }
1096}
1097
1098fn emit_ret_to_napi(
1099    out: &mut String,
1100    ty: &TypeRef,
1101    module: &str,
1102    prefix: &str,
1103    fn_name: &str,
1104    structs: &HashMap<String, StructBinding>,
1105) {
1106    out.push_str("  napi_value ret;\n");
1107    match ty {
1108        TypeRef::I32 => out.push_str("  napi_create_int32(env, result, &ret);\n"),
1109        TypeRef::U32 => out.push_str("  napi_create_uint32(env, result, &ret);\n"),
1110        TypeRef::I64 => out.push_str("  napi_create_int64(env, result, &ret);\n"),
1111        TypeRef::F64 => out.push_str("  napi_create_double(env, result, &ret);\n"),
1112        TypeRef::Bool => out.push_str("  napi_get_boolean(env, result, &ret);\n"),
1113        TypeRef::StringUtf8 => {
1114            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1115            out.push_str("  weaveffi_free_string(result);\n");
1116        }
1117        TypeRef::BorrowedStr => {
1118            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1119        }
1120        TypeRef::TypedHandle(_) | TypeRef::Handle => {
1121            out.push_str("  napi_create_int64(env, (int64_t)result, &ret);\n");
1122        }
1123        TypeRef::Struct(name) => {
1124            emit_struct_to_object(
1125                out, "env", name, "result", "ret", module, prefix, structs, "  ", true,
1126            );
1127        }
1128        TypeRef::Enum(_) => {
1129            out.push_str("  napi_create_int32(env, (int32_t)result, &ret);\n");
1130        }
1131        TypeRef::Bytes => {
1132            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1133            out.push_str("  weaveffi_free_bytes((uint8_t*)result, out_len);\n");
1134        }
1135        TypeRef::BorrowedBytes => {
1136            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
1137        }
1138        TypeRef::Optional(inner) => {
1139            out.push_str("  if (result == NULL) {\n");
1140            out.push_str("    napi_get_null(env, &ret);\n");
1141            out.push_str("  } else {\n");
1142            emit_optional_ret_inner(out, inner, module, prefix, structs);
1143            out.push_str("  }\n");
1144        }
1145        TypeRef::List(inner) => emit_list_ret(out, inner, module, prefix, "  ", structs),
1146        TypeRef::Map(_, _) => {
1147            out.push_str("  napi_create_object(env, &ret);\n");
1148        }
1149        TypeRef::Iterator(inner) => {
1150            let fn_pascal = fn_name.to_upper_camel_case();
1151            let iter_type = format!("{prefix}_{module}_{fn_pascal}Iterator");
1152            let et = c_elem_type(inner, module, prefix);
1153            out.push_str("  napi_create_array(env, &ret);\n");
1154            out.push_str("  uint32_t iter_idx = 0;\n");
1155            out.push_str(&format!("  {et} iter_item;\n"));
1156            out.push_str(&format!(
1157                "  while ({iter_type}_next(result, &iter_item)) {{\n"
1158            ));
1159            out.push_str("    napi_value elem;\n");
1160            match inner.as_ref() {
1161                TypeRef::I32 => {
1162                    out.push_str("    napi_create_int32(env, iter_item, &elem);\n");
1163                }
1164                TypeRef::U32 => {
1165                    out.push_str("    napi_create_uint32(env, iter_item, &elem);\n");
1166                }
1167                TypeRef::I64 => {
1168                    out.push_str("    napi_create_int64(env, iter_item, &elem);\n");
1169                }
1170                TypeRef::F64 => {
1171                    out.push_str("    napi_create_double(env, iter_item, &elem);\n");
1172                }
1173                TypeRef::Bool => {
1174                    out.push_str("    napi_get_boolean(env, iter_item, &elem);\n");
1175                }
1176                TypeRef::TypedHandle(_) | TypeRef::Handle => {
1177                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1178                }
1179                TypeRef::StringUtf8 => {
1180                    out.push_str(
1181                        "    napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
1182                    );
1183                    out.push_str("    weaveffi_free_string(iter_item);\n");
1184                }
1185                TypeRef::Struct(_) | TypeRef::Enum(_) => {
1186                    out.push_str(
1187                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
1188                    );
1189                }
1190                _ => {
1191                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
1192                }
1193            }
1194            out.push_str("    napi_set_element(env, ret, iter_idx++, elem);\n");
1195            out.push_str("  }\n");
1196            out.push_str(&format!("  {iter_type}_destroy(result);\n"));
1197        }
1198    }
1199    out.push_str("  return ret;\n");
1200}
1201
1202fn emit_optional_ret_inner(
1203    out: &mut String,
1204    inner: &TypeRef,
1205    module: &str,
1206    prefix: &str,
1207    structs: &HashMap<String, StructBinding>,
1208) {
1209    match inner {
1210        TypeRef::I32 => {
1211            out.push_str("    napi_create_int32(env, *result, &ret);\n");
1212            out.push_str("    free(result);\n");
1213        }
1214        TypeRef::U32 => {
1215            out.push_str("    napi_create_uint32(env, *result, &ret);\n");
1216            out.push_str("    free(result);\n");
1217        }
1218        TypeRef::I64 => {
1219            out.push_str("    napi_create_int64(env, *result, &ret);\n");
1220            out.push_str("    free(result);\n");
1221        }
1222        TypeRef::F64 => {
1223            out.push_str("    napi_create_double(env, *result, &ret);\n");
1224            out.push_str("    free(result);\n");
1225        }
1226        TypeRef::Bool => {
1227            out.push_str("    napi_get_boolean(env, *result, &ret);\n");
1228            out.push_str("    free(result);\n");
1229        }
1230        TypeRef::TypedHandle(_) | TypeRef::Handle => {
1231            out.push_str("    napi_create_int64(env, (int64_t)*result, &ret);\n");
1232            out.push_str("    free(result);\n");
1233        }
1234        TypeRef::Enum(_) => {
1235            out.push_str("    napi_create_int32(env, (int32_t)*result, &ret);\n");
1236            out.push_str("    free(result);\n");
1237        }
1238        TypeRef::StringUtf8 => {
1239            out.push_str("    napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
1240            out.push_str("    weaveffi_free_string(result);\n");
1241        }
1242        TypeRef::Struct(name) => {
1243            emit_struct_to_object(
1244                out, "env", name, "result", "ret", module, prefix, structs, "    ", true,
1245            );
1246        }
1247        TypeRef::List(li) => emit_list_ret(out, li, module, prefix, "    ", structs),
1248        _ => out.push_str("    napi_get_null(env, &ret);\n"),
1249    }
1250}
1251
1252fn emit_list_ret(
1253    out: &mut String,
1254    inner: &TypeRef,
1255    module: &str,
1256    prefix: &str,
1257    ind: &str,
1258    structs: &HashMap<String, StructBinding>,
1259) {
1260    out.push_str(&format!(
1261        "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
1262    ));
1263    out.push_str(&format!(
1264        "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
1265    ));
1266    out.push_str(&format!("{ind}  napi_value elem;\n"));
1267    match inner {
1268        TypeRef::I32 => out.push_str(&format!(
1269            "{ind}  napi_create_int32(env, result[ret_i], &elem);\n"
1270        )),
1271        TypeRef::U32 => out.push_str(&format!(
1272            "{ind}  napi_create_uint32(env, result[ret_i], &elem);\n"
1273        )),
1274        TypeRef::I64 => out.push_str(&format!(
1275            "{ind}  napi_create_int64(env, result[ret_i], &elem);\n"
1276        )),
1277        TypeRef::F64 => out.push_str(&format!(
1278            "{ind}  napi_create_double(env, result[ret_i], &elem);\n"
1279        )),
1280        TypeRef::Bool => out.push_str(&format!(
1281            "{ind}  napi_get_boolean(env, result[ret_i], &elem);\n"
1282        )),
1283        TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
1284            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1285        )),
1286        TypeRef::StringUtf8 => {
1287            out.push_str(&format!(
1288                "{ind}  napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
1289            ));
1290            out.push_str(&format!("{ind}  weaveffi_free_string(result[ret_i]);\n"));
1291        }
1292        TypeRef::Enum(_) => out.push_str(&format!(
1293            "{ind}  napi_create_int32(env, (int32_t)result[ret_i], &elem);\n"
1294        )),
1295        TypeRef::Struct(name) => {
1296            let elem_indent = format!("{ind}  ");
1297            emit_struct_to_object(
1298                out,
1299                "env",
1300                name,
1301                "result[ret_i]",
1302                "elem",
1303                module,
1304                prefix,
1305                structs,
1306                &elem_indent,
1307                true,
1308            );
1309        }
1310        _ => out.push_str(&format!(
1311            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
1312        )),
1313    }
1314    out.push_str(&format!(
1315        "{ind}  napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
1316    ));
1317    out.push_str(&format!("{ind}}}\n"));
1318    out.push_str(&format!("{ind}free(result);\n"));
1319}
1320
1321fn ts_type_for(ty: &TypeRef) -> String {
1322    match ty {
1323        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
1324        TypeRef::Bool => "boolean".into(),
1325        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
1326        TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
1327        TypeRef::Handle => "bigint".into(),
1328        TypeRef::TypedHandle(name) => name.clone(),
1329        TypeRef::Struct(name) => local_type_name(name).to_string(),
1330        TypeRef::Enum(name) => name.clone(),
1331        TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
1332        TypeRef::List(inner) => {
1333            let inner_ts = ts_type_for(inner);
1334            if matches!(inner.as_ref(), TypeRef::Optional(_)) {
1335                format!("({inner_ts})[]")
1336            } else {
1337                format!("{inner_ts}[]")
1338            }
1339        }
1340        TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
1341        TypeRef::Iterator(inner) => {
1342            let t = ts_type_for(inner);
1343            format!("{t}[]")
1344        }
1345    }
1346}
1347
1348/// Emits a JSDoc comment at `indent`. Single-line docs collapse to
1349/// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines.
1350fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
1351    common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc);
1352}
1353
1354/// Emits a JSDoc block for a function: function doc, `@param name desc` for
1355/// each documented parameter, and an optional trailing tag list.
1356fn emit_fn_doc(
1357    out: &mut String,
1358    doc: &Option<String>,
1359    params: &[ParamBinding],
1360    indent: &str,
1361    extra_tags: &[String],
1362) {
1363    let has_param_docs = params.iter().any(|p| p.doc.is_some());
1364    let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
1365    if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() {
1366        return;
1367    }
1368    out.push_str(indent);
1369    out.push_str("/**\n");
1370    if let Some(d) = trimmed_doc {
1371        for line in d.lines() {
1372            out.push_str(indent);
1373            if line.is_empty() {
1374                out.push_str(" *\n");
1375            } else {
1376                out.push_str(" * ");
1377                out.push_str(line);
1378                out.push('\n');
1379            }
1380        }
1381    }
1382    for p in params {
1383        if let Some(pdoc) = &p.doc {
1384            let pdoc = pdoc.trim();
1385            if pdoc.is_empty() {
1386                continue;
1387            }
1388            let mut lines = pdoc.lines();
1389            if let Some(first) = lines.next() {
1390                out.push_str(indent);
1391                out.push_str(&format!(" * @param {} {}\n", p.name, first));
1392            }
1393            for line in lines {
1394                out.push_str(indent);
1395                if line.is_empty() {
1396                    out.push_str(" *\n");
1397                } else {
1398                    out.push_str(" *   ");
1399                    out.push_str(line);
1400                    out.push('\n');
1401                }
1402            }
1403        }
1404    }
1405    for tag in extra_tags {
1406        out.push_str(indent);
1407        out.push_str(" * ");
1408        out.push_str(tag);
1409        out.push('\n');
1410    }
1411    out.push_str(indent);
1412    out.push_str(" */\n");
1413}
1414
1415fn render_struct_builder_dts(out: &mut String, s: &StructBinding) {
1416    let name = &s.name;
1417    emit_doc(out, &s.doc, "");
1418    out.push_str(&format!("export interface {}Builder {{\n", s.name));
1419    for field in &s.fields {
1420        let method = format!("with{}", field.name.to_upper_camel_case());
1421        let ts = ts_type_for(&field.ty);
1422        emit_doc(out, &field.doc, "  ");
1423        out.push_str(&format!("  {method}(value: {ts}): {name}Builder;\n"));
1424    }
1425    out.push_str(&format!("  build(): {name};\n"));
1426    out.push_str("}\n");
1427}
1428
1429fn render_node_dts(
1430    api: &Api,
1431    prefix: &str,
1432    strip_module_prefix: bool,
1433    input_basename: &str,
1434) -> String {
1435    let model = BindingModel::build(api, prefix);
1436    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
1437    out.push_str("// Generated types for WeaveFFI functions\n");
1438    for m in &model.modules {
1439        for s in &m.structs {
1440            emit_doc(&mut out, &s.doc, "");
1441            out.push_str(&format!("export interface {} {{\n", s.name));
1442            for field in &s.fields {
1443                emit_doc(&mut out, &field.doc, "  ");
1444                out.push_str(&format!("  {}: {};\n", field.name, ts_type_for(&field.ty)));
1445            }
1446            out.push_str("}\n");
1447            if s.builder.is_some() {
1448                render_struct_builder_dts(&mut out, s);
1449            }
1450        }
1451        for e in &m.enums {
1452            emit_doc(&mut out, &e.doc, "");
1453            out.push_str(&format!("export enum {} {{\n", e.name));
1454            for v in &e.variants {
1455                emit_doc(&mut out, &v.doc, "  ");
1456                out.push_str(&format!("  {} = {},\n", v.name, v.value));
1457            }
1458            out.push_str("}\n");
1459        }
1460        out.push_str(&format!("// module {}\n", m.path));
1461        for f in &m.functions {
1462            let params: Vec<String> = f
1463                .params
1464                .iter()
1465                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
1466                .collect();
1467            let base_ret = match &f.ret {
1468                Some(ty) => ts_type_for(ty),
1469                None => "void".into(),
1470            };
1471            let ret = if f.is_async {
1472                format!("Promise<{base_ret}>")
1473            } else {
1474                base_ret
1475            };
1476            let ts_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
1477            let mut tags = vec![format!("Maps to C function: {}", f.c_base)];
1478            if let Some(msg) = &f.deprecated {
1479                tags.push(format!("@deprecated {}", msg));
1480            }
1481            emit_fn_doc(&mut out, &f.doc, &f.params, "", &tags);
1482            out.push_str(&format!(
1483                "export function {}({}): {}\n",
1484                ts_name,
1485                params.join(", "),
1486                ret
1487            ));
1488        }
1489    }
1490    out.push('\n');
1491    out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts"));
1492    out
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497    use super::*;
1498    use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
1499
1500    fn make_api(modules: Vec<Module>) -> Api {
1501        Api {
1502            version: "0.1.0".into(),
1503            modules,
1504            generators: None,
1505        }
1506    }
1507
1508    fn make_module(name: &str) -> Module {
1509        Module {
1510            name: name.into(),
1511            functions: vec![],
1512            structs: vec![],
1513            enums: vec![],
1514            callbacks: vec![],
1515            listeners: vec![],
1516            errors: None,
1517            modules: vec![],
1518        }
1519    }
1520
1521    #[test]
1522    fn ts_type_for_primitives() {
1523        assert_eq!(ts_type_for(&TypeRef::I32), "number");
1524        assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
1525        assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
1526        assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
1527        assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
1528    }
1529
1530    #[test]
1531    fn ts_type_for_struct_and_enum() {
1532        assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
1533        assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
1534    }
1535
1536    #[test]
1537    fn ts_type_for_optional() {
1538        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1539        assert_eq!(ts_type_for(&ty), "string | null");
1540    }
1541
1542    #[test]
1543    fn ts_type_for_list() {
1544        let ty = TypeRef::List(Box::new(TypeRef::I32));
1545        assert_eq!(ts_type_for(&ty), "number[]");
1546    }
1547
1548    #[test]
1549    fn ts_type_for_list_of_optional() {
1550        let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
1551        assert_eq!(ts_type_for(&ty), "(number | null)[]");
1552    }
1553
1554    #[test]
1555    fn ts_type_for_map() {
1556        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1557        assert_eq!(ts_type_for(&ty), "Record<string, number>");
1558    }
1559
1560    #[test]
1561    fn ts_type_for_optional_list() {
1562        let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
1563        assert_eq!(ts_type_for(&ty), "number[] | null");
1564    }
1565
1566    #[test]
1567    fn generate_node_dts_with_structs() {
1568        let mut m = make_module("contacts");
1569        m.structs.push(StructDef {
1570            name: "Contact".into(),
1571            doc: None,
1572            fields: vec![
1573                StructField {
1574                    name: "name".into(),
1575                    ty: TypeRef::StringUtf8,
1576                    doc: None,
1577                    default: None,
1578                },
1579                StructField {
1580                    name: "age".into(),
1581                    ty: TypeRef::I32,
1582                    doc: None,
1583                    default: None,
1584                },
1585                StructField {
1586                    name: "active".into(),
1587                    ty: TypeRef::Bool,
1588                    doc: None,
1589                    default: None,
1590                },
1591            ],
1592            builder: false,
1593        });
1594        m.enums.push(EnumDef {
1595            name: "Color".into(),
1596            doc: None,
1597            variants: vec![
1598                EnumVariant {
1599                    name: "Red".into(),
1600                    value: 0,
1601                    doc: None,
1602                },
1603                EnumVariant {
1604                    name: "Green".into(),
1605                    value: 1,
1606                    doc: None,
1607                },
1608                EnumVariant {
1609                    name: "Blue".into(),
1610                    value: 2,
1611                    doc: None,
1612                },
1613            ],
1614        });
1615        m.functions.push(Function {
1616            name: "get_contact".into(),
1617            params: vec![Param {
1618                name: "id".into(),
1619                ty: TypeRef::I32,
1620                mutable: false,
1621                doc: None,
1622            }],
1623            returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1624                "Contact".into(),
1625            )))),
1626            doc: None,
1627            r#async: false,
1628            cancellable: false,
1629            deprecated: None,
1630            since: None,
1631        });
1632        m.functions.push(Function {
1633            name: "list_contacts".into(),
1634            params: vec![],
1635            returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1636            doc: None,
1637            r#async: false,
1638            cancellable: false,
1639            deprecated: None,
1640            since: None,
1641        });
1642
1643        let dts = render_node_dts(&make_api(vec![m]), "weaveffi", true, "weaveffi.yml");
1644
1645        assert!(dts.contains("export interface Contact {"));
1646        assert!(dts.contains("  name: string;"));
1647        assert!(dts.contains("  age: number;"));
1648        assert!(dts.contains("  active: boolean;"));
1649        assert!(dts.contains("export enum Color {"));
1650        assert!(dts.contains("  Red = 0,"));
1651        assert!(dts.contains("  Green = 1,"));
1652        assert!(dts.contains("  Blue = 2,"));
1653        assert!(dts.contains("export function get_contact(id: number): Contact | null"));
1654        assert!(dts.contains("export function list_contacts(): Contact[]"));
1655
1656        let iface_pos = dts.find("export interface Contact").unwrap();
1657        let enum_pos = dts.find("export enum Color").unwrap();
1658        let fn_pos = dts.find("export function get_contact").unwrap();
1659        assert!(
1660            iface_pos < fn_pos,
1661            "interface should appear before functions"
1662        );
1663        assert!(enum_pos < fn_pos, "enum should appear before functions");
1664    }
1665
1666    #[test]
1667    fn node_generates_binding_gyp() {
1668        let api = make_api(vec![{
1669            let mut m = make_module("math");
1670            m.functions.push(Function {
1671                name: "add".into(),
1672                params: vec![
1673                    Param {
1674                        name: "a".into(),
1675                        ty: TypeRef::I32,
1676                        mutable: false,
1677                        doc: None,
1678                    },
1679                    Param {
1680                        name: "b".into(),
1681                        ty: TypeRef::I32,
1682                        mutable: false,
1683                        doc: None,
1684                    },
1685                ],
1686                returns: Some(TypeRef::I32),
1687                doc: None,
1688                r#async: false,
1689                cancellable: false,
1690                deprecated: None,
1691                since: None,
1692            });
1693            m
1694        }]);
1695
1696        let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
1697        let _ = std::fs::remove_dir_all(&tmp);
1698        std::fs::create_dir_all(&tmp).unwrap();
1699        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1700
1701        NodeGenerator
1702            .generate(&api, out_dir, &NodeConfig::default())
1703            .unwrap();
1704
1705        let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
1706        assert!(
1707            gyp.contains("\"target_name\": \"weaveffi\""),
1708            "missing target_name: {gyp}"
1709        );
1710        assert!(
1711            gyp.contains("weaveffi_addon.c"),
1712            "missing source file: {gyp}"
1713        );
1714
1715        let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
1716        assert!(
1717            addon.contains("napi_value Init("),
1718            "missing Init function: {addon}"
1719        );
1720        assert!(
1721            addon.contains("weaveffi_math_add"),
1722            "missing C ABI call: {addon}"
1723        );
1724        assert!(
1725            addon.contains("napi_get_cb_info"),
1726            "missing napi_get_cb_info call: {addon}"
1727        );
1728
1729        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1730        assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
1731        assert!(
1732            pkg.contains("node-gyp rebuild"),
1733            "missing install script: {pkg}"
1734        );
1735
1736        let _ = std::fs::remove_dir_all(&tmp);
1737    }
1738
1739    #[test]
1740    fn generate_node_dts_with_structs_and_enums() {
1741        let api = make_api(vec![Module {
1742            name: "contacts".to_string(),
1743            functions: vec![
1744                Function {
1745                    name: "get_contact".to_string(),
1746                    params: vec![Param {
1747                        name: "id".to_string(),
1748                        ty: TypeRef::I32,
1749                        mutable: false,
1750                        doc: None,
1751                    }],
1752                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1753                        "Contact".into(),
1754                    )))),
1755                    doc: None,
1756                    r#async: false,
1757                    cancellable: false,
1758                    deprecated: None,
1759                    since: None,
1760                },
1761                Function {
1762                    name: "list_contacts".to_string(),
1763                    params: vec![],
1764                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
1765                    doc: None,
1766                    r#async: false,
1767                    cancellable: false,
1768                    deprecated: None,
1769                    since: None,
1770                },
1771                Function {
1772                    name: "set_favorite_color".to_string(),
1773                    params: vec![
1774                        Param {
1775                            name: "contact_id".to_string(),
1776                            ty: TypeRef::I32,
1777                            mutable: false,
1778                            doc: None,
1779                        },
1780                        Param {
1781                            name: "color".to_string(),
1782                            ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
1783                            mutable: false,
1784                            doc: None,
1785                        },
1786                    ],
1787                    returns: None,
1788                    doc: None,
1789                    r#async: false,
1790                    cancellable: false,
1791                    deprecated: None,
1792                    since: None,
1793                },
1794                Function {
1795                    name: "get_tags".to_string(),
1796                    params: vec![Param {
1797                        name: "contact_id".to_string(),
1798                        ty: TypeRef::I32,
1799                        mutable: false,
1800                        doc: None,
1801                    }],
1802                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
1803                    doc: None,
1804                    r#async: false,
1805                    cancellable: false,
1806                    deprecated: None,
1807                    since: None,
1808                },
1809            ],
1810            structs: vec![StructDef {
1811                name: "Contact".to_string(),
1812                doc: None,
1813                fields: vec![
1814                    StructField {
1815                        name: "name".to_string(),
1816                        ty: TypeRef::StringUtf8,
1817                        doc: None,
1818                        default: None,
1819                    },
1820                    StructField {
1821                        name: "email".to_string(),
1822                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
1823                        doc: None,
1824                        default: None,
1825                    },
1826                    StructField {
1827                        name: "tags".to_string(),
1828                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
1829                        doc: None,
1830                        default: None,
1831                    },
1832                ],
1833                builder: false,
1834            }],
1835            enums: vec![EnumDef {
1836                name: "Color".to_string(),
1837                doc: None,
1838                variants: vec![
1839                    EnumVariant {
1840                        name: "Red".to_string(),
1841                        value: 0,
1842                        doc: None,
1843                    },
1844                    EnumVariant {
1845                        name: "Green".to_string(),
1846                        value: 1,
1847                        doc: None,
1848                    },
1849                    EnumVariant {
1850                        name: "Blue".to_string(),
1851                        value: 2,
1852                        doc: None,
1853                    },
1854                ],
1855            }],
1856            callbacks: vec![],
1857            listeners: vec![],
1858            errors: None,
1859            modules: vec![],
1860        }]);
1861
1862        let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
1863        let _ = std::fs::remove_dir_all(&tmp);
1864        std::fs::create_dir_all(&tmp).unwrap();
1865        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1866
1867        NodeGenerator
1868            .generate(
1869                &api,
1870                out_dir,
1871                &NodeConfig {
1872                    strip_module_prefix: true,
1873                    ..NodeConfig::default()
1874                },
1875            )
1876            .unwrap();
1877
1878        let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
1879
1880        assert!(
1881            dts.contains("export interface Contact {"),
1882            "missing Contact interface: {dts}"
1883        );
1884        assert!(dts.contains("  name: string;"), "missing name field: {dts}");
1885        assert!(
1886            dts.contains("  email: string | null;"),
1887            "missing optional email field: {dts}"
1888        );
1889        assert!(
1890            dts.contains("  tags: string[];"),
1891            "missing list tags field: {dts}"
1892        );
1893
1894        assert!(
1895            dts.contains("export enum Color {"),
1896            "missing Color enum: {dts}"
1897        );
1898        assert!(dts.contains("  Red = 0,"), "missing Red variant: {dts}");
1899        assert!(dts.contains("  Green = 1,"), "missing Green variant: {dts}");
1900        assert!(dts.contains("  Blue = 2,"), "missing Blue variant: {dts}");
1901
1902        assert!(
1903            dts.contains("export function get_contact(id: number): Contact | null"),
1904            "missing get_contact with optional return: {dts}"
1905        );
1906        assert!(
1907            dts.contains("export function list_contacts(): Contact[]"),
1908            "missing list_contacts with list return: {dts}"
1909        );
1910        assert!(
1911            dts.contains(
1912                "export function set_favorite_color(contact_id: number, color: Color | null): void"
1913            ),
1914            "missing set_favorite_color with optional enum param: {dts}"
1915        );
1916        assert!(
1917            dts.contains("export function get_tags(contact_id: number): string[]"),
1918            "missing get_tags with list return: {dts}"
1919        );
1920
1921        let iface_pos = dts.find("export interface Contact").unwrap();
1922        let enum_pos = dts.find("export enum Color").unwrap();
1923        let fn_pos = dts.find("export function get_contact").unwrap();
1924        assert!(
1925            iface_pos < fn_pos,
1926            "interface should appear before functions"
1927        );
1928        assert!(enum_pos < fn_pos, "enum should appear before functions");
1929
1930        let _ = std::fs::remove_dir_all(&tmp);
1931    }
1932
1933    #[test]
1934    fn node_custom_package_name() {
1935        let api = make_api(vec![make_module("math")]);
1936
1937        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
1938        let _ = std::fs::remove_dir_all(&tmp);
1939        std::fs::create_dir_all(&tmp).unwrap();
1940        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
1941
1942        let config = NodeConfig {
1943            package_name: Some("@myorg/cool-lib".into()),
1944            ..NodeConfig::default()
1945        };
1946        NodeGenerator.generate(&api, out_dir, &config).unwrap();
1947
1948        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
1949        assert!(
1950            pkg.contains("\"name\": \"@myorg/cool-lib\""),
1951            "package.json should use custom name: {pkg}"
1952        );
1953        assert!(
1954            !pkg.contains("\"name\": \"weaveffi\""),
1955            "package.json should not contain default name: {pkg}"
1956        );
1957
1958        let _ = std::fs::remove_dir_all(&tmp);
1959    }
1960
1961    #[test]
1962    fn node_dts_has_jsdoc() {
1963        let api = make_api(vec![{
1964            let mut m = make_module("math");
1965            m.functions.push(Function {
1966                name: "add".into(),
1967                params: vec![
1968                    Param {
1969                        name: "a".into(),
1970                        ty: TypeRef::I32,
1971                        mutable: false,
1972                        doc: None,
1973                    },
1974                    Param {
1975                        name: "b".into(),
1976                        ty: TypeRef::I32,
1977                        mutable: false,
1978                        doc: None,
1979                    },
1980                ],
1981                returns: Some(TypeRef::I32),
1982                doc: None,
1983                r#async: false,
1984                cancellable: false,
1985                deprecated: None,
1986                since: None,
1987            });
1988            m.functions.push(Function {
1989                name: "subtract".into(),
1990                params: vec![
1991                    Param {
1992                        name: "a".into(),
1993                        ty: TypeRef::I32,
1994                        mutable: false,
1995                        doc: None,
1996                    },
1997                    Param {
1998                        name: "b".into(),
1999                        ty: TypeRef::I32,
2000                        mutable: false,
2001                        doc: None,
2002                    },
2003                ],
2004                returns: Some(TypeRef::I32),
2005                doc: None,
2006                r#async: false,
2007                cancellable: false,
2008                deprecated: None,
2009                since: None,
2010            });
2011            m
2012        }]);
2013
2014        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2015
2016        assert!(
2017            dts.contains("Maps to C function: weaveffi_math_add"),
2018            "missing JSDoc for add: {dts}"
2019        );
2020        assert!(
2021            dts.contains("Maps to C function: weaveffi_math_subtract"),
2022            "missing JSDoc for subtract: {dts}"
2023        );
2024    }
2025
2026    #[test]
2027    fn node_addon_has_no_todo() {
2028        let api = make_api(vec![{
2029            let mut m = make_module("math");
2030            m.functions.push(Function {
2031                name: "add".into(),
2032                params: vec![
2033                    Param {
2034                        name: "a".into(),
2035                        ty: TypeRef::I32,
2036                        mutable: false,
2037                        doc: None,
2038                    },
2039                    Param {
2040                        name: "b".into(),
2041                        ty: TypeRef::I32,
2042                        mutable: false,
2043                        doc: None,
2044                    },
2045                ],
2046                returns: Some(TypeRef::I32),
2047                doc: None,
2048                r#async: false,
2049                cancellable: false,
2050                deprecated: None,
2051                since: None,
2052            });
2053            m
2054        }]);
2055        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2056        assert!(
2057            !addon.contains("// TODO: implement"),
2058            "generated addon.c should not contain TODO comments: {addon}"
2059        );
2060    }
2061
2062    #[test]
2063    fn node_addon_extracts_args() {
2064        let api = make_api(vec![{
2065            let mut m = make_module("math");
2066            m.functions.push(Function {
2067                name: "add".into(),
2068                params: vec![
2069                    Param {
2070                        name: "a".into(),
2071                        ty: TypeRef::I32,
2072                        mutable: false,
2073                        doc: None,
2074                    },
2075                    Param {
2076                        name: "b".into(),
2077                        ty: TypeRef::I32,
2078                        mutable: false,
2079                        doc: None,
2080                    },
2081                ],
2082                returns: Some(TypeRef::I32),
2083                doc: None,
2084                r#async: false,
2085                cancellable: false,
2086                deprecated: None,
2087                since: None,
2088            });
2089            m
2090        }]);
2091        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2092        assert!(
2093            addon.contains("napi_get_cb_info"),
2094            "generated addon.c should call napi_get_cb_info: {addon}"
2095        );
2096    }
2097
2098    #[test]
2099    fn node_addon_frees_strings() {
2100        let api = make_api(vec![{
2101            let mut m = make_module("greet");
2102            m.functions.push(Function {
2103                name: "hello".into(),
2104                params: vec![Param {
2105                    name: "name".into(),
2106                    ty: TypeRef::StringUtf8,
2107                    mutable: false,
2108                    doc: None,
2109                }],
2110                returns: Some(TypeRef::StringUtf8),
2111                doc: None,
2112                r#async: false,
2113                cancellable: false,
2114                deprecated: None,
2115                since: None,
2116            });
2117            m
2118        }]);
2119        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2120        assert!(
2121            addon.contains("weaveffi_free_string(result)"),
2122            "generated addon should free returned strings: {addon}"
2123        );
2124        assert!(
2125            addon.contains("#include <string.h>"),
2126            "generated addon should include string.h: {addon}"
2127        );
2128        assert!(
2129            addon.contains("#include <stdlib.h>"),
2130            "generated addon should include stdlib.h: {addon}"
2131        );
2132        assert!(
2133            addon.contains("weaveffi_error_clear(&err)"),
2134            "generated addon should clear errors: {addon}"
2135        );
2136    }
2137
2138    #[test]
2139    fn node_custom_prefix_threads_to_user_symbols() {
2140        let api = make_api(vec![{
2141            let mut m = make_module("greet");
2142            m.functions.push(Function {
2143                name: "hello".into(),
2144                params: vec![Param {
2145                    name: "name".into(),
2146                    ty: TypeRef::StringUtf8,
2147                    mutable: false,
2148                    doc: None,
2149                }],
2150                returns: Some(TypeRef::StringUtf8),
2151                doc: None,
2152                r#async: false,
2153                cancellable: false,
2154                deprecated: None,
2155                since: None,
2156            });
2157            m
2158        }]);
2159
2160        let config = NodeConfig {
2161            prefix: Some("myffi".into()),
2162            ..NodeConfig::default()
2163        };
2164
2165        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_prefix");
2166        let _ = std::fs::remove_dir_all(&tmp);
2167        std::fs::create_dir_all(&tmp).unwrap();
2168        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2169
2170        NodeGenerator.generate(&api, out_dir, &config).unwrap();
2171
2172        // The output file name is a fixed library artifact name, not the ABI
2173        // prefix, so it stays `weaveffi_addon.c` regardless of `prefix`.
2174        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
2175
2176        // User symbols pick up the configured ABI prefix.
2177        assert!(
2178            addon.contains("myffi_greet_hello"),
2179            "addon should call the prefixed user symbol myffi_greet_hello: {addon}"
2180        );
2181        assert!(
2182            !addon.contains("weaveffi_greet_hello"),
2183            "addon must not emit the hard-coded weaveffi_ user symbol: {addon}"
2184        );
2185        assert!(
2186            addon.contains("#include \"myffi.h\""),
2187            "addon should include the prefixed header myffi.h: {addon}"
2188        );
2189
2190        // Runtime ABI helpers are supplied by weaveffi-abi and stay literal.
2191        assert!(
2192            addon.contains("weaveffi_error"),
2193            "runtime weaveffi_error must remain literal: {addon}"
2194        );
2195        assert!(
2196            addon.contains("weaveffi_free_string"),
2197            "runtime weaveffi_free_string must remain literal: {addon}"
2198        );
2199
2200        let _ = std::fs::remove_dir_all(&tmp);
2201    }
2202
2203    #[test]
2204    fn node_addon_checks_error() {
2205        let api = make_api(vec![{
2206            let mut m = make_module("math");
2207            m.functions.push(Function {
2208                name: "add".into(),
2209                params: vec![
2210                    Param {
2211                        name: "a".into(),
2212                        ty: TypeRef::I32,
2213                        mutable: false,
2214                        doc: None,
2215                    },
2216                    Param {
2217                        name: "b".into(),
2218                        ty: TypeRef::I32,
2219                        mutable: false,
2220                        doc: None,
2221                    },
2222                ],
2223                returns: Some(TypeRef::I32),
2224                doc: None,
2225                r#async: false,
2226                cancellable: false,
2227                deprecated: None,
2228                since: None,
2229            });
2230            m
2231        }]);
2232        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2233        assert!(
2234            addon.contains("err.code"),
2235            "generated addon.c should check err.code: {addon}"
2236        );
2237    }
2238
2239    #[test]
2240    fn node_strip_module_prefix() {
2241        let api = make_api(vec![{
2242            let mut m = make_module("contacts");
2243            m.functions.push(Function {
2244                name: "create_contact".into(),
2245                params: vec![Param {
2246                    name: "name".into(),
2247                    ty: TypeRef::StringUtf8,
2248                    mutable: false,
2249                    doc: None,
2250                }],
2251                returns: Some(TypeRef::I32),
2252                doc: None,
2253                r#async: false,
2254                cancellable: false,
2255                deprecated: None,
2256                since: None,
2257            });
2258            m
2259        }]);
2260
2261        let config = NodeConfig {
2262            strip_module_prefix: true,
2263            ..NodeConfig::default()
2264        };
2265
2266        let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
2267        let _ = std::fs::remove_dir_all(&tmp);
2268        std::fs::create_dir_all(&tmp).unwrap();
2269        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
2270
2271        NodeGenerator.generate(&api, out_dir, &config).unwrap();
2272
2273        let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
2274        assert!(
2275            dts.contains("export function create_contact("),
2276            "stripped name should be create_contact: {dts}"
2277        );
2278        assert!(
2279            !dts.contains("export function contacts_create_contact("),
2280            "should not contain module-prefixed name: {dts}"
2281        );
2282
2283        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
2284        assert!(
2285            addon.contains("\"create_contact\""),
2286            "JS export name should be stripped: {addon}"
2287        );
2288        assert!(
2289            addon.contains("weaveffi_contacts_create_contact"),
2290            "C ABI call should still use full name: {addon}"
2291        );
2292
2293        let no_strip = NodeConfig::default();
2294        let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
2295        let _ = std::fs::remove_dir_all(&tmp2);
2296        std::fs::create_dir_all(&tmp2).unwrap();
2297        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
2298
2299        NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap();
2300
2301        let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
2302        assert!(
2303            dts2.contains("export function contacts_create_contact("),
2304            "default should use module-prefixed name: {dts2}"
2305        );
2306
2307        let _ = std::fs::remove_dir_all(&tmp);
2308        let _ = std::fs::remove_dir_all(&tmp2);
2309    }
2310
2311    #[test]
2312    fn node_typed_handle_type() {
2313        let api = make_api(vec![{
2314            let mut m = make_module("contacts");
2315            m.structs.push(StructDef {
2316                name: "Contact".into(),
2317                doc: None,
2318                fields: vec![StructField {
2319                    name: "name".into(),
2320                    ty: TypeRef::StringUtf8,
2321                    doc: None,
2322                    default: None,
2323                }],
2324                builder: false,
2325            });
2326            m.functions.push(Function {
2327                name: "get_info".into(),
2328                params: vec![Param {
2329                    name: "contact".into(),
2330                    ty: TypeRef::TypedHandle("Contact".into()),
2331                    mutable: false,
2332                    doc: None,
2333                }],
2334                returns: None,
2335                doc: None,
2336                r#async: false,
2337                cancellable: false,
2338                deprecated: None,
2339                since: None,
2340            });
2341            m
2342        }]);
2343        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2344        assert!(
2345            dts.contains("contact: Contact"),
2346            "TypedHandle should use class type not bigint: {dts}"
2347        );
2348    }
2349
2350    #[test]
2351    fn node_deeply_nested_optional() {
2352        let api = make_api(vec![Module {
2353            name: "edge".into(),
2354            functions: vec![Function {
2355                name: "process".into(),
2356                params: vec![Param {
2357                    name: "data".into(),
2358                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2359                        Box::new(TypeRef::Struct("Contact".into())),
2360                    ))))),
2361                    mutable: false,
2362                    doc: None,
2363                }],
2364                returns: None,
2365                doc: None,
2366                r#async: false,
2367                cancellable: false,
2368                deprecated: None,
2369                since: None,
2370            }],
2371            structs: vec![StructDef {
2372                name: "Contact".into(),
2373                doc: None,
2374                fields: vec![StructField {
2375                    name: "name".into(),
2376                    ty: TypeRef::StringUtf8,
2377                    doc: None,
2378                    default: None,
2379                }],
2380                builder: false,
2381            }],
2382            enums: vec![],
2383            callbacks: vec![],
2384            listeners: vec![],
2385            errors: None,
2386            modules: vec![],
2387        }]);
2388        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2389        assert!(
2390            dts.contains("(Contact | null)[] | null"),
2391            "should contain deeply nested optional type: {dts}"
2392        );
2393    }
2394
2395    #[test]
2396    fn node_map_of_lists() {
2397        let api = make_api(vec![Module {
2398            name: "edge".into(),
2399            functions: vec![Function {
2400                name: "process".into(),
2401                params: vec![Param {
2402                    name: "scores".into(),
2403                    ty: TypeRef::Map(
2404                        Box::new(TypeRef::StringUtf8),
2405                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2406                    ),
2407                    mutable: false,
2408                    doc: None,
2409                }],
2410                returns: None,
2411                doc: None,
2412                r#async: false,
2413                cancellable: false,
2414                deprecated: None,
2415                since: None,
2416            }],
2417            structs: vec![],
2418            enums: vec![],
2419            callbacks: vec![],
2420            listeners: vec![],
2421            errors: None,
2422            modules: vec![],
2423        }]);
2424        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2425        assert!(
2426            dts.contains("Record<string, number[]>"),
2427            "should contain map of lists type: {dts}"
2428        );
2429    }
2430
2431    #[test]
2432    fn node_enum_keyed_map() {
2433        let api = make_api(vec![Module {
2434            name: "edge".into(),
2435            functions: vec![Function {
2436                name: "process".into(),
2437                params: vec![Param {
2438                    name: "contacts".into(),
2439                    ty: TypeRef::Map(
2440                        Box::new(TypeRef::Enum("Color".into())),
2441                        Box::new(TypeRef::Struct("Contact".into())),
2442                    ),
2443                    mutable: false,
2444                    doc: None,
2445                }],
2446                returns: None,
2447                doc: None,
2448                r#async: false,
2449                cancellable: false,
2450                deprecated: None,
2451                since: None,
2452            }],
2453            structs: vec![StructDef {
2454                name: "Contact".into(),
2455                doc: None,
2456                fields: vec![StructField {
2457                    name: "name".into(),
2458                    ty: TypeRef::StringUtf8,
2459                    doc: None,
2460                    default: None,
2461                }],
2462                builder: false,
2463            }],
2464            enums: vec![EnumDef {
2465                name: "Color".into(),
2466                doc: None,
2467                variants: vec![
2468                    EnumVariant {
2469                        name: "Red".into(),
2470                        value: 0,
2471                        doc: None,
2472                    },
2473                    EnumVariant {
2474                        name: "Green".into(),
2475                        value: 1,
2476                        doc: None,
2477                    },
2478                    EnumVariant {
2479                        name: "Blue".into(),
2480                        value: 2,
2481                        doc: None,
2482                    },
2483                ],
2484            }],
2485            callbacks: vec![],
2486            listeners: vec![],
2487            errors: None,
2488            modules: vec![],
2489        }]);
2490        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2491        assert!(
2492            dts.contains("Record<Color, Contact>"),
2493            "should contain enum-keyed map type: {dts}"
2494        );
2495    }
2496
2497    #[test]
2498    fn node_no_double_free_on_error() {
2499        let api = make_api(vec![{
2500            let mut m = make_module("contacts");
2501            m.structs.push(StructDef {
2502                name: "Contact".into(),
2503                doc: None,
2504                fields: vec![StructField {
2505                    name: "name".into(),
2506                    ty: TypeRef::StringUtf8,
2507                    doc: None,
2508                    default: None,
2509                }],
2510                builder: false,
2511            });
2512            m.functions.push(Function {
2513                name: "find_contact".into(),
2514                params: vec![Param {
2515                    name: "name".into(),
2516                    ty: TypeRef::StringUtf8,
2517                    mutable: false,
2518                    doc: None,
2519                }],
2520                returns: Some(TypeRef::Struct("Contact".into())),
2521                doc: None,
2522                r#async: false,
2523                cancellable: false,
2524                deprecated: None,
2525                since: None,
2526            });
2527            m
2528        }]);
2529        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2530        assert!(
2531            addon.contains("free(name)"),
2532            "malloc'd JS string copy should be freed after the C call: {addon}"
2533        );
2534        assert!(
2535            !addon.contains("weaveffi_free_string(name)"),
2536            "input string param must not use weaveffi_free_string: {addon}"
2537        );
2538        let free_pos = addon
2539            .find("free(name)")
2540            .expect("free(name) should be present");
2541        let err_pos = addon
2542            .find("if (err.code != 0)")
2543            .expect("err.code check should be present");
2544        assert!(
2545            free_pos < err_pos,
2546            "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
2547        );
2548        let err_block_start = addon
2549            .find("  if (err.code != 0) {\n")
2550            .expect("error if block should be present");
2551        let after_err = &addon[err_block_start..];
2552        let err_block_end_rel = after_err
2553            .find("  }\n  napi_value ret;")
2554            .expect("napi_value ret should follow error block");
2555        let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
2556        assert!(
2557            !err_block.contains("result"),
2558            "error path should not touch result before return NULL: {err_block}"
2559        );
2560    }
2561
2562    #[test]
2563    fn node_null_check_on_optional_return() {
2564        let api = make_api(vec![{
2565            let mut m = make_module("contacts");
2566            m.structs.push(StructDef {
2567                name: "Contact".into(),
2568                doc: None,
2569                fields: vec![StructField {
2570                    name: "name".into(),
2571                    ty: TypeRef::StringUtf8,
2572                    doc: None,
2573                    default: None,
2574                }],
2575                builder: false,
2576            });
2577            m.functions.push(Function {
2578                name: "find_contact".into(),
2579                params: vec![Param {
2580                    name: "id".into(),
2581                    ty: TypeRef::I32,
2582                    mutable: false,
2583                    doc: None,
2584                }],
2585                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2586                    "Contact".into(),
2587                )))),
2588                doc: None,
2589                r#async: false,
2590                cancellable: false,
2591                deprecated: None,
2592                since: None,
2593            });
2594            m
2595        }]);
2596        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2597        assert!(
2598            addon.contains("if (result == NULL)"),
2599            "optional struct return should null-check before wrapping: {addon}"
2600        );
2601        assert!(
2602            addon.contains("napi_get_null"),
2603            "optional absent should return JS null via napi_get_null: {addon}"
2604        );
2605    }
2606
2607    #[test]
2608    fn node_async_returns_promise() {
2609        let api = make_api(vec![{
2610            let mut m = make_module("tasks");
2611            m.functions.push(Function {
2612                name: "run".into(),
2613                params: vec![Param {
2614                    name: "id".into(),
2615                    ty: TypeRef::I32,
2616                    mutable: false,
2617                    doc: None,
2618                }],
2619                returns: Some(TypeRef::StringUtf8),
2620                doc: None,
2621                r#async: true,
2622                cancellable: false,
2623                deprecated: None,
2624                since: None,
2625            });
2626            m.functions.push(Function {
2627                name: "fire_and_forget".into(),
2628                params: vec![],
2629                returns: None,
2630                doc: None,
2631                r#async: true,
2632                cancellable: false,
2633                deprecated: None,
2634                since: None,
2635            });
2636            m
2637        }]);
2638        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
2639        assert!(
2640            dts.contains("Promise<"),
2641            "async function should return Promise in .d.ts: {dts}"
2642        );
2643        assert!(
2644            dts.contains("): Promise<string>"),
2645            "async string return should be Promise<string>: {dts}"
2646        );
2647        assert!(
2648            dts.contains("): Promise<void>"),
2649            "async void return should be Promise<void>: {dts}"
2650        );
2651    }
2652
2653    #[test]
2654    fn node_addon_creates_promise() {
2655        let api = make_api(vec![{
2656            let mut m = make_module("tasks");
2657            m.functions.push(Function {
2658                name: "run".into(),
2659                params: vec![Param {
2660                    name: "id".into(),
2661                    ty: TypeRef::I32,
2662                    mutable: false,
2663                    doc: None,
2664                }],
2665                returns: Some(TypeRef::I32),
2666                doc: None,
2667                r#async: true,
2668                cancellable: false,
2669                deprecated: None,
2670                since: None,
2671            });
2672            m
2673        }]);
2674        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2675        assert!(
2676            addon.contains("napi_create_promise"),
2677            "async addon should call napi_create_promise: {addon}"
2678        );
2679        assert!(
2680            addon.contains("napi_resolve_deferred"),
2681            "async callback should call napi_resolve_deferred: {addon}"
2682        );
2683        assert!(
2684            addon.contains("napi_reject_deferred"),
2685            "async callback should call napi_reject_deferred: {addon}"
2686        );
2687        assert!(
2688            addon.contains("weaveffi_napi_async_ctx"),
2689            "async addon should define async context struct: {addon}"
2690        );
2691        assert!(
2692            addon.contains("weaveffi_tasks_run_async("),
2693            "async addon should call the _async C function: {addon}"
2694        );
2695        assert!(
2696            addon.contains("weaveffi_tasks_run_napi_cb"),
2697            "async addon should define the callback: {addon}"
2698        );
2699    }
2700
2701    /// The N-API deferred is created with `napi_create_promise` and consumed
2702    /// (and freed) by exactly one of `napi_resolve_deferred` /
2703    /// `napi_reject_deferred`. The async context struct that carries the
2704    /// deferred across the C callback boundary must be `malloc`-ed once and
2705    /// `free`-d exactly once on the callback path.
2706    #[test]
2707    fn node_async_pins_callback_for_lifetime() {
2708        let api = make_api(vec![{
2709            let mut m = make_module("tasks");
2710            m.functions.push(Function {
2711                name: "run".into(),
2712                params: vec![Param {
2713                    name: "id".into(),
2714                    ty: TypeRef::I32,
2715                    mutable: false,
2716                    doc: None,
2717                }],
2718                returns: Some(TypeRef::I32),
2719                doc: None,
2720                r#async: true,
2721                cancellable: false,
2722                deprecated: None,
2723                since: None,
2724            });
2725            m
2726        }]);
2727        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
2728        let create_count = addon.matches("napi_create_promise").count();
2729        let resolve_count = addon.matches("napi_resolve_deferred").count();
2730        let reject_count = addon.matches("napi_reject_deferred").count();
2731        let malloc_count = addon
2732            .matches("malloc(sizeof(weaveffi_napi_async_ctx))")
2733            .count();
2734        let free_count = addon.matches("free(ctx);").count();
2735        assert_eq!(
2736            create_count, 1,
2737            "expected one napi_create_promise per async fn, got {create_count}: {addon}"
2738        );
2739        assert_eq!(
2740            resolve_count, 1,
2741            "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}"
2742        );
2743        assert_eq!(
2744            reject_count, 1,
2745            "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}"
2746        );
2747        assert_eq!(
2748            malloc_count, free_count,
2749            "ctx malloc / free must balance per async fn: malloc={malloc_count} free={free_count}: {addon}"
2750        );
2751    }
2752
2753    fn doc_module() -> Module {
2754        Module {
2755            name: "docs".into(),
2756            functions: vec![Function {
2757                name: "do_thing".into(),
2758                params: vec![Param {
2759                    name: "x".into(),
2760                    ty: TypeRef::I32,
2761                    mutable: false,
2762                    doc: Some("the input value".into()),
2763                }],
2764                returns: Some(TypeRef::I32),
2765                doc: Some("Performs a thing.".into()),
2766                r#async: false,
2767                cancellable: false,
2768                deprecated: None,
2769                since: None,
2770            }],
2771            structs: vec![StructDef {
2772                name: "Item".into(),
2773                doc: Some("An item we track.".into()),
2774                fields: vec![StructField {
2775                    name: "id".into(),
2776                    ty: TypeRef::I64,
2777                    doc: Some("Stable id".into()),
2778                    default: None,
2779                }],
2780                builder: false,
2781            }],
2782            enums: vec![EnumDef {
2783                name: "Kind".into(),
2784                doc: Some("Kind of item.".into()),
2785                variants: vec![EnumVariant {
2786                    name: "Small".into(),
2787                    value: 0,
2788                    doc: Some("A small one".into()),
2789                }],
2790            }],
2791            callbacks: vec![],
2792            listeners: vec![],
2793            errors: None,
2794            modules: vec![],
2795        }
2796    }
2797
2798    #[test]
2799    fn node_emits_doc_on_function() {
2800        let dts = render_node_dts(
2801            &make_api(vec![doc_module()]),
2802            "weaveffi",
2803            true,
2804            "weaveffi.yml",
2805        );
2806        assert!(dts.contains("Performs a thing."), "{dts}");
2807    }
2808
2809    #[test]
2810    fn node_emits_doc_on_struct() {
2811        let dts = render_node_dts(
2812            &make_api(vec![doc_module()]),
2813            "weaveffi",
2814            true,
2815            "weaveffi.yml",
2816        );
2817        assert!(dts.contains("/** An item we track. */"), "{dts}");
2818    }
2819
2820    #[test]
2821    fn node_emits_doc_on_enum_variant() {
2822        let dts = render_node_dts(
2823            &make_api(vec![doc_module()]),
2824            "weaveffi",
2825            true,
2826            "weaveffi.yml",
2827        );
2828        assert!(dts.contains("/** Kind of item. */"), "{dts}");
2829        assert!(dts.contains("/** A small one */"), "{dts}");
2830    }
2831
2832    #[test]
2833    fn node_emits_doc_on_field() {
2834        let dts = render_node_dts(
2835            &make_api(vec![doc_module()]),
2836            "weaveffi",
2837            true,
2838            "weaveffi.yml",
2839        );
2840        assert!(dts.contains("/** Stable id */"), "{dts}");
2841    }
2842
2843    #[test]
2844    fn node_emits_doc_on_param() {
2845        let dts = render_node_dts(
2846            &make_api(vec![doc_module()]),
2847            "weaveffi",
2848            true,
2849            "weaveffi.yml",
2850        );
2851        assert!(dts.contains("@param x the input value"), "{dts}");
2852    }
2853}