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