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