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