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