Skip to main content

weaveffi_gen_node/
lib.rs

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