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