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