1use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::{format_ident, quote};
6use syn::{
7 parse_macro_input, FnArg, GenericArgument, ItemFn, LitStr, Pat, PathArguments, ReturnType,
8 Token, Type,
9};
10
11fn is_str_ref(ty: &Type) -> bool {
16 matches!(ty, Type::Reference(r) if matches!(r.elem.as_ref(), Type::Path(p) if p.path.is_ident("str")))
17}
18
19fn is_string(ty: &Type) -> bool {
20 matches!(ty, Type::Path(p) if p.path.is_ident("String"))
21}
22
23fn is_bool(ty: &Type) -> bool {
24 matches!(ty, Type::Path(p) if p.path.is_ident("bool"))
25}
26
27fn is_numeric(ty: &Type) -> bool {
28 if let Type::Path(p) = ty {
29 if let Some(ident) = p.path.get_ident() {
30 return matches!(
31 ident.to_string().as_str(),
32 "u8" | "u16"
33 | "u32"
34 | "u64"
35 | "i8"
36 | "i16"
37 | "i32"
38 | "i64"
39 | "f32"
40 | "f64"
41 | "usize"
42 | "isize"
43 );
44 }
45 }
46 false
47}
48
49fn unwrap_single_generic<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
50 if let Type::Path(p) = ty {
51 let last = p.path.segments.last()?;
52 if last.ident != name {
53 return None;
54 }
55 if let PathArguments::AngleBracketed(args) = &last.arguments {
56 if let Some(GenericArgument::Type(inner)) = args.args.first() {
57 return Some(inner);
58 }
59 }
60 }
61 None
62}
63
64fn unwrap_option(ty: &Type) -> Option<&Type> {
65 unwrap_single_generic(ty, "Option")
66}
67
68fn unwrap_vec(ty: &Type) -> Option<&Type> {
69 unwrap_single_generic(ty, "Vec")
70}
71
72fn unwrap_result(ty: &Type) -> Option<(&Type, &Type)> {
73 if let Type::Path(p) = ty {
74 let last = p.path.segments.last()?;
75 if last.ident != "Result" {
76 return None;
77 }
78 if let PathArguments::AngleBracketed(args) = &last.arguments {
79 let mut iter = args.args.iter();
80 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
81 (iter.next(), iter.next())
82 {
83 return Some((ok, err));
84 }
85 }
86 }
87 None
88}
89
90fn contains_nested_reference(ty: &Type) -> bool {
91 match ty {
92 Type::Reference(_) => true,
93 Type::Array(a) => contains_nested_reference(&a.elem),
94 Type::Group(g) => contains_nested_reference(&g.elem),
95 Type::Paren(p) => contains_nested_reference(&p.elem),
96 Type::Slice(s) => contains_nested_reference(&s.elem),
97 Type::Tuple(t) => t.elems.iter().any(contains_nested_reference),
98 Type::Path(p) => p.path.segments.iter().any(|seg| {
99 if let PathArguments::AngleBracketed(args) = &seg.arguments {
100 args.args.iter().any(|arg| match arg {
101 GenericArgument::Type(inner) => contains_nested_reference(inner),
102 _ => false,
103 })
104 } else {
105 false
106 }
107 }),
108 _ => false,
109 }
110}
111
112fn js_param_name(ident: &syn::Ident) -> String {
114 let s = ident.to_string();
115 s.strip_prefix("r#").unwrap_or(&s).to_string()
116}
117
118fn wasm_param(name: &syn::Ident, ty: &Type) -> (TokenStream2, TokenStream2) {
126 if is_str_ref(ty) {
127 (quote!(#name: &str), quote!())
129 } else if let Type::Reference(r) = ty {
130 if r.mutability.is_some() {
131 return (
132 syn::Error::new_spanned(
133 ty,
134 "#[wasm_export] does not support &mut T params; take T by value",
135 )
136 .to_compile_error(),
137 quote!(),
138 );
139 }
140 if matches!(r.elem.as_ref(), Type::Slice(_)) {
141 return (
142 syn::Error::new_spanned(
143 ty,
144 "#[wasm_export] does not support &[T] params; use Vec<T>",
145 )
146 .to_compile_error(),
147 quote!(),
148 );
149 }
150 let inner = &*r.elem;
152 let owned = format_ident!("{name}_owned_");
153 (
154 quote!(#name: ::wasm_bindgen::JsValue),
155 quote!(
156 let #owned: #inner = ::serde_wasm_bindgen::from_value(#name)
157 .map_err(|e| ::wasm_bindgen::JsError::new(&e.to_string()))?;
158 let #name = &#owned;
159 ),
160 )
161 } else if is_string(ty) {
162 (quote!(#name: String), quote!())
163 } else if is_bool(ty) {
164 (quote!(#name: bool), quote!())
165 } else if is_numeric(ty) {
166 (quote!(#name: #ty), quote!())
167 } else if contains_nested_reference(ty) {
168 (
169 syn::Error::new_spanned(
170 ty,
171 "#[wasm_export] does not support borrowed references inside generic/container types (e.g. Option<&T>, Vec<&T>); use owned data",
172 )
173 .to_compile_error(),
174 quote!(),
175 )
176 } else {
177 (
178 quote!(#name: ::wasm_bindgen::JsValue),
179 quote!(
180 let #name: #ty = ::serde_wasm_bindgen::from_value(#name)
181 .map_err(|e| ::wasm_bindgen::JsError::new(&e.to_string()))?;
182 ),
183 )
184 }
185}
186
187fn wasm_return_body(ret_ty: &Type, call: TokenStream2) -> TokenStream2 {
188 if let Some((_, err_ty)) = unwrap_result(ret_ty) {
189 let err_conv = if is_string(err_ty) {
190 quote!(|e| ::wasm_bindgen::JsError::new(&e))
191 } else {
192 quote!(|e| ::wasm_bindgen::JsError::new(&e.to_string()))
193 };
194 quote!(
195 let __result = #call.map_err(#err_conv)?;
196 let __serializer = ::serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
197 ::serde::Serialize::serialize(&__result, &__serializer)
198 .map_err(|e| ::wasm_bindgen::JsError::new(&e.to_string()))
199 )
200 } else {
201 quote!(
202 let __result = #call;
203 let __serializer = ::serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
204 ::serde::Serialize::serialize(&__result, &__serializer)
205 .map_err(|e| ::wasm_bindgen::JsError::new(&e.to_string()))
206 )
207 }
208}
209
210#[derive(Clone, Copy)]
215enum Dialect {
216 Ts,
217 Flow,
218}
219
220fn type_expr(ty: &Type, dialect: Dialect) -> TokenStream2 {
226 if is_str_ref(ty) || is_string(ty) {
227 quote!("string".to_string())
228 } else if let Type::Reference(r) = ty {
229 type_expr(&r.elem, dialect)
230 } else if is_bool(ty) {
231 quote!("boolean".to_string())
232 } else if let Some(inner) = unwrap_vec(ty) {
233 let inner_expr = type_expr(inner, dialect);
234 match dialect {
235 Dialect::Ts => quote!(format!("ReadonlyArray<{}>", #inner_expr)),
236 Dialect::Flow => quote!(format!("$ReadOnlyArray<{}>", #inner_expr)),
237 }
238 } else if let Some(inner) = unwrap_option(ty) {
239 let inner_expr = type_expr(inner, dialect);
240 match dialect {
241 Dialect::Ts => quote!(format!("{} | null", #inner_expr)),
242 Dialect::Flow => quote!(format!("?{}", #inner_expr)),
243 }
244 } else if let Some((ok_ty, _)) = unwrap_result(ty) {
245 type_expr(ok_ty, dialect)
246 } else {
247 match dialect {
248 Dialect::Ts => quote!(<#ty as ::ts_rs::TS>::name(&cfg)),
249 Dialect::Flow => quote!(<#ty as ::flowjs_rs::Flow>::name(&cfg)),
250 }
251 }
252}
253
254fn deref_ty(ty: &Type) -> &Type {
256 if let Type::Reference(r) = ty {
257 &r.elem
258 } else {
259 ty
260 }
261}
262
263fn params_body(params: &[(syn::Ident, Type)], dialect: Dialect) -> TokenStream2 {
264 if params.is_empty() {
265 return quote!(String::new());
266 }
267 let parts: Vec<TokenStream2> = params
268 .iter()
269 .enumerate()
270 .map(|(i, (name, ty))| {
271 let name_str = js_param_name(name);
272 if let Some(inner) = unwrap_option(deref_ty(ty)) {
273 let inner_expr = type_expr(inner, dialect);
274 match dialect {
275 Dialect::Ts => {
276 let all_remaining_optional = params[i..]
278 .iter()
279 .all(|(_, t)| unwrap_option(deref_ty(t)).is_some());
280 if all_remaining_optional {
281 quote!(format!("{}?: {} | null", #name_str, #inner_expr))
282 } else {
283 quote!(format!("{}: {} | null | undefined", #name_str, #inner_expr))
284 }
285 }
286 Dialect::Flow => quote!(format!("{}: ?{}", #name_str, #inner_expr)),
287 }
288 } else {
289 let expr = type_expr(ty, dialect);
290 quote!(format!("{}: {}", #name_str, #expr))
291 }
292 })
293 .collect();
294 quote!([#(#parts),*].join(", "))
295}
296
297fn snake_to_camel(s: &str) -> String {
302 let mut result = String::new();
303 let mut capitalize_next = false;
304 for c in s.chars() {
305 if c == '_' {
306 capitalize_next = true;
307 } else if capitalize_next {
308 result.push(c.to_ascii_uppercase());
309 capitalize_next = false;
310 } else {
311 result.push(c);
312 }
313 }
314 result
315}
316
317fn snake_to_screaming(s: &str) -> String {
318 s.to_uppercase()
319}
320
321#[proc_macro_attribute]
342pub fn wasm_export(attr: TokenStream, item: TokenStream) -> TokenStream {
343 if !attr.is_empty() {
344 return syn::Error::new(
345 proc_macro2::Span::call_site(),
346 "#[wasm_export] takes no arguments -- it reads the Rust function signature directly",
347 )
348 .to_compile_error()
349 .into();
350 }
351
352 let func = parse_macro_input!(item as ItemFn);
353
354 if func.sig.asyncness.is_some() {
355 return syn::Error::new_spanned(
356 &func.sig,
357 "#[wasm_export] does not support async functions",
358 )
359 .to_compile_error()
360 .into();
361 }
362 if !func.sig.generics.params.is_empty() {
363 return syn::Error::new_spanned(
364 &func.sig.generics,
365 "#[wasm_export] does not support generic functions",
366 )
367 .to_compile_error()
368 .into();
369 }
370
371 let fn_name = &func.sig.ident;
372 let fn_name_str = fn_name.to_string();
373 let fn_name_bare = fn_name_str.strip_prefix("r#").unwrap_or(&fn_name_str);
374 let js_name = snake_to_camel(fn_name_bare);
375 let wasm_fn_name = format_ident!("__wasm_{}", fn_name);
376 let const_name = format_ident!(
377 "_WASM_JS_BRIDGE_{}",
378 snake_to_screaming(&fn_name.to_string())
379 );
380 let ts_params_fn = format_ident!("__wjb_ts_params_{}", fn_name);
381 let ts_ret_fn = format_ident!("__wjb_ts_ret_{}", fn_name);
382 let flow_params_fn = format_ident!("__wjb_flow_params_{}", fn_name);
383 let flow_ret_fn = format_ident!("__wjb_flow_ret_{}", fn_name);
384
385 let mut typed_params: Vec<(syn::Ident, Type)> = Vec::new();
387 for arg in &func.sig.inputs {
388 match arg {
389 FnArg::Receiver(r) => {
390 return syn::Error::new_spanned(
391 r,
392 "#[wasm_export] does not support `self` receivers",
393 )
394 .to_compile_error()
395 .into();
396 }
397 FnArg::Typed(pt) => match pt.pat.as_ref() {
398 Pat::Ident(pi) => typed_params.push((pi.ident.clone(), *pt.ty.clone())),
399 _ => {
400 return syn::Error::new_spanned(
401 &pt.pat,
402 "#[wasm_export] requires simple identifier patterns; destructuring and `_` are not supported",
403 )
404 .to_compile_error()
405 .into();
406 }
407 },
408 }
409 }
410
411 let ret_ty: Type = match &func.sig.output {
413 ReturnType::Default => syn::parse_quote!(()),
414 ReturnType::Type(_, ty) => *ty.clone(),
415 };
416
417 let (wasm_param_decls, wasm_deser_stmts): (Vec<_>, Vec<_>) = typed_params
419 .iter()
420 .map(|(name, ty)| wasm_param(name, ty))
421 .unzip();
422
423 let param_idents: Vec<&syn::Ident> = typed_params.iter().map(|(n, _)| n).collect();
425 let call_expr = quote!(#fn_name(#(#param_idents),*));
426
427 let wasm_body = wasm_return_body(&ret_ty, call_expr);
429
430 let ts_params_expr = params_body(&typed_params, Dialect::Ts);
432 let ts_ret_expr = type_expr(&ret_ty, Dialect::Ts);
433 let flow_params_expr = params_body(&typed_params, Dialect::Flow);
434 let flow_ret_expr = type_expr(&ret_ty, Dialect::Flow);
435
436 let non_doc_attrs: Vec<_> = func
443 .attrs
444 .iter()
445 .filter(|a| {
446 let path = a.path();
447 !path.is_ident("doc") && !path.is_ident("must_use")
448 })
449 .collect();
450 let all_attrs = &func.attrs;
451
452 let vis = &func.vis;
453 let sig = &func.sig;
454 let block = &func.block;
455
456 let descriptor_cfg = quote!(all(
460 feature = "codegen",
461 any(feature = "ts", feature = "flow")
462 ));
463
464 quote! {
465 #(#all_attrs)*
467 #vis #sig #block
468
469 #(#non_doc_attrs)*
473 #[cfg(feature = "wasm")]
474 #[::wasm_bindgen::prelude::wasm_bindgen(js_name = #js_name)]
475 pub fn #wasm_fn_name(
476 #(#wasm_param_decls),*
477 ) -> ::std::result::Result<::wasm_bindgen::JsValue, ::wasm_bindgen::JsError> {
478 #(#wasm_deser_stmts)*
479 #wasm_body
480 }
481
482 #(#non_doc_attrs)*
485 #[cfg(all(feature = "codegen", feature = "ts"))]
486 fn #ts_params_fn() -> String {
487 let cfg: ::ts_rs::Config = ::std::default::Default::default();
488 #ts_params_expr
489 }
490 #(#non_doc_attrs)*
491 #[cfg(all(feature = "codegen", not(feature = "ts")))]
492 fn #ts_params_fn() -> String {
493 "any".to_string()
494 }
495 #(#non_doc_attrs)*
496 #[cfg(all(feature = "codegen", feature = "ts"))]
497 fn #ts_ret_fn() -> String {
498 let cfg: ::ts_rs::Config = ::std::default::Default::default();
499 #ts_ret_expr
500 }
501 #(#non_doc_attrs)*
502 #[cfg(all(feature = "codegen", not(feature = "ts")))]
503 fn #ts_ret_fn() -> String {
504 "any".to_string()
505 }
506 #(#non_doc_attrs)*
507 #[cfg(all(feature = "codegen", feature = "flow"))]
508 fn #flow_params_fn() -> String {
509 let cfg: ::flowjs_rs::Config = ::std::default::Default::default();
510 #flow_params_expr
511 }
512 #(#non_doc_attrs)*
513 #[cfg(all(feature = "codegen", not(feature = "flow")))]
514 fn #flow_params_fn() -> String {
515 "any".to_string()
516 }
517 #(#non_doc_attrs)*
518 #[cfg(all(feature = "codegen", feature = "flow"))]
519 fn #flow_ret_fn() -> String {
520 let cfg: ::flowjs_rs::Config = ::std::default::Default::default();
521 #flow_ret_expr
522 }
523 #(#non_doc_attrs)*
524 #[cfg(all(feature = "codegen", not(feature = "flow")))]
525 fn #flow_ret_fn() -> String {
526 "any".to_string()
527 }
528 #(#non_doc_attrs)*
529 #[cfg(#descriptor_cfg)]
530 #[doc(hidden)]
531 #[allow(dead_code)]
532 #vis const #const_name: ::wasm_js_bridge::WasmFn = ::wasm_js_bridge::WasmFn {
533 name: #js_name,
534 file: file!(),
535 ts_params: #ts_params_fn,
536 ts_ret: #ts_ret_fn,
537 flow_params: #flow_params_fn,
538 flow_ret: #flow_ret_fn,
539 };
540 }
541 .into()
542}
543
544struct BundleArgs {
549 types: Vec<syn::Type>,
550 fns: Vec<syn::Ident>,
551 aliases: Vec<(String, String)>,
552 opaque: Vec<(String, Option<String>)>,
553}
554
555impl syn::parse::Parse for BundleArgs {
556 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
557 let mut types = Vec::new();
558 let mut fns = Vec::new();
559 let mut aliases = Vec::new();
560 let mut opaque = Vec::new();
561
562 while !input.is_empty() {
563 let key: syn::Ident = input.parse()?;
564 input.parse::<Token![=]>()?;
565
566 match key.to_string().as_str() {
567 "types" => {
568 let content;
569 syn::bracketed!(content in input);
570 while !content.is_empty() {
571 types.push(content.parse::<syn::Type>()?);
572 if !content.is_empty() {
573 content.parse::<Token![,]>()?;
574 }
575 }
576 }
577 "fns" => {
578 let content;
579 syn::bracketed!(content in input);
580 while !content.is_empty() {
581 fns.push(content.parse::<syn::Ident>()?);
582 if !content.is_empty() {
583 content.parse::<Token![,]>()?;
584 }
585 }
586 }
587 "aliases" => {
588 let content;
589 syn::bracketed!(content in input);
590 while !content.is_empty() {
591 let tuple;
592 syn::parenthesized!(tuple in content);
593 let name: LitStr = tuple.parse()?;
594 tuple.parse::<Token![,]>()?;
595 let target: LitStr = tuple.parse()?;
596 aliases.push((name.value(), target.value()));
597 if !content.is_empty() {
598 content.parse::<Token![,]>()?;
599 }
600 }
601 }
602 "opaque" => {
603 let content;
604 syn::bracketed!(content in input);
605 while !content.is_empty() {
606 let tuple;
607 syn::parenthesized!(tuple in content);
608 let name: LitStr = tuple.parse()?;
609 tuple.parse::<Token![,]>()?;
610 let ident: syn::Ident = tuple.parse()?;
612 let bound = if ident == "None" {
613 None
614 } else if ident == "Some" {
615 let inner;
616 syn::parenthesized!(inner in tuple);
617 let lit: LitStr = inner.parse()?;
618 Some(lit.value())
619 } else {
620 return Err(syn::Error::new(
621 ident.span(),
622 "expected `None` or `Some(\"bound\")`",
623 ));
624 };
625 opaque.push((name.value(), bound));
626 if !content.is_empty() {
627 content.parse::<Token![,]>()?;
628 }
629 }
630 }
631 _ => {
632 return Err(syn::Error::new(key.span(), format!("unknown key: {key}")));
633 }
634 }
635
636 if !input.is_empty() {
637 input.parse::<Token![,]>()?;
638 }
639 }
640
641 Ok(BundleArgs {
642 types,
643 fns,
644 aliases,
645 opaque,
646 })
647 }
648}
649
650#[proc_macro]
680pub fn wasm_peers(_input: TokenStream) -> TokenStream {
681 let shim_path = match std::env::var("WJB_PEER_SHIM") {
682 Ok(p) => p,
683 Err(_) => return TokenStream::new(),
684 };
685
686 let content = match std::fs::read_to_string(&shim_path) {
687 Ok(s) => s,
688 Err(e) => {
689 return syn::Error::new(
690 proc_macro2::Span::call_site(),
691 format!("Failed to read WJB_PEER_SHIM {shim_path}: {e}"),
692 )
693 .to_compile_error()
694 .into()
695 }
696 };
697
698 match content.parse::<TokenStream2>() {
699 Ok(ts) => ts.into(),
700 Err(e) => syn::Error::new(
701 proc_macro2::Span::call_site(),
702 format!("Invalid peer shim: {e}"),
703 )
704 .to_compile_error()
705 .into(),
706 }
707}
708
709#[proc_macro]
710pub fn bundle(input: TokenStream) -> TokenStream {
711 let args = parse_macro_input!(input as BundleArgs);
712
713 let mod_name = {
716 let span = proc_macro2::Span::call_site();
717 let src = format!("{span:?}");
718 let hash: u64 = src
719 .bytes()
720 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
721 format_ident!("__wjb_bundle_{:016x}", hash)
722 };
723
724 let types = &args.types;
726
727 let export_consts: Vec<syn::Ident> = args
729 .fns
730 .iter()
731 .map(|f| format_ident!("_WASM_JS_BRIDGE_{}", snake_to_screaming(&f.to_string())))
732 .collect();
733
734 let alias_items: Vec<TokenStream2> = args
736 .aliases
737 .iter()
738 .map(|(name, target)| quote!(::wasm_js_bridge::TypeAlias { name: #name, target: #target }))
739 .collect();
740
741 let opaque_items: Vec<TokenStream2> = args
743 .opaque
744 .iter()
745 .map(|(name, bound)| match bound {
746 Some(b) => quote!(::wasm_js_bridge::OpaqueType { name: #name, bound: Some(#b) }),
747 None => quote!(::wasm_js_bridge::OpaqueType { name: #name, bound: None }),
748 })
749 .collect();
750
751 quote! {
752 #[cfg(all(test, feature = "codegen", any(feature = "ts", feature = "flow")))]
753 #[allow(non_snake_case)]
754 mod #mod_name {
755 use super::*;
756
757 #[test]
758 fn generate_npm_files() {
759 #[cfg(feature = "ts")]
760 use ::ts_rs::TS as _;
761 #[cfg(feature = "flow")]
762 use ::flowjs_rs::Flow as _;
763
764 #[cfg(feature = "ts")]
765 let ts_decls: ::std::vec::Vec<::std::string::String> = {
766 let ts_cfg: ::ts_rs::Config = ::std::default::Default::default();
767 ::std::vec![
768 #(<#types as ::ts_rs::TS>::decl(&ts_cfg)),*
769 ]
770 };
771
772 #[cfg(feature = "flow")]
773 let flow_decls: ::std::vec::Vec<::std::string::String> = {
774 let flow_cfg: ::flowjs_rs::Config = ::std::default::Default::default();
775 ::std::vec![
776 #(<#types as ::flowjs_rs::Flow>::decl(&flow_cfg)),*
777 ]
778 };
779
780 let aliases: &[::wasm_js_bridge::TypeAlias] = &[#(#alias_items),*];
781 let opaque: &[::wasm_js_bridge::OpaqueType] = &[#(#opaque_items),*];
782
783 let all_fns: &[::wasm_js_bridge::WasmFn] = &[
784 #(#export_consts),*
785 ];
786
787 let dir = match ::std::env::var("WJB_OUT_DIR") {
789 Ok(d) if !d.is_empty() => ::std::path::PathBuf::from(d),
790 _ => ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")),
791 };
792 ::std::fs::create_dir_all(&dir).expect("Failed to create output directory");
793
794 let mut by_stem: ::std::collections::BTreeMap<
797 ::std::string::String,
798 ::std::vec::Vec<::wasm_js_bridge::WasmFn>,
799 > = ::std::default::Default::default();
800
801 let mut stem_to_file: ::std::collections::BTreeMap<
804 ::std::string::String,
805 &'static str,
806 > = ::std::default::Default::default();
807 for f in all_fns {
808 let s = ::wasm_js_bridge::file_to_stem(f.file);
809 if let Some(existing_file) = stem_to_file.get(&s) {
810 if *existing_file != f.file {
811 panic!(
812 "wasm-js-bridge bundle!: stem collision — \"{}\" and \"{}\" \
813 both produce stem \"{}\". Rename one of the source files.",
814 existing_file, f.file, s
815 );
816 }
817 } else {
818 stem_to_file.insert(s.clone(), f.file);
819 }
820 by_stem.entry(s).or_default().push(*f);
821 }
822
823 if by_stem.is_empty() {
825 let stem = ::wasm_js_bridge::file_to_stem(file!());
826 by_stem.insert(stem, ::std::vec::Vec::new());
827 }
828
829 for (stem, fns) in &by_stem {
830 #[cfg(feature = "ts")]
831 {
832 let dts = ::wasm_js_bridge::generate_index_dts(&ts_decls, aliases, &[], fns);
833 ::std::fs::write(dir.join(format!("{stem}.d.ts")), dts)
834 .expect("Failed to write .d.ts");
835 }
836 #[cfg(feature = "flow")]
837 {
838 let flow = ::wasm_js_bridge::generate_index_flow(&flow_decls, aliases, &[], fns, opaque);
839 ::std::fs::write(dir.join(format!("{stem}.js.flow")), flow)
840 .expect("Failed to write .js.flow");
841 }
842 }
843 }
844 }
845 }
846 .into()
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn snake_to_camel_basic() {
855 assert_eq!(snake_to_camel("parse_selector"), "parseSelector", "basic");
857 assert_eq!(snake_to_camel("select"), "select", "no underscore");
858 assert_eq!(
859 snake_to_camel("diff_annotations"),
860 "diffAnnotations",
861 "two words"
862 );
863 assert_eq!(
864 snake_to_camel("extract_aql_symbols"),
865 "extractAqlSymbols",
866 "three words"
867 );
868 }
869
870 #[test]
871 fn snake_to_screaming_basic() {
872 assert_eq!(
874 snake_to_screaming("parse_selector"),
875 "PARSE_SELECTOR",
876 "underscore preserved"
877 );
878 assert_eq!(snake_to_screaming("select"), "SELECT", "single word");
879 }
880
881 #[test]
882 fn detects_nested_reference_types() {
883 let ty_option_ref: Type = syn::parse_quote!(Option<&str>);
885 let ty_vec_ref: Type = syn::parse_quote!(Vec<&MyType>);
886 let ty_owned: Type = syn::parse_quote!(Option<String>);
887
888 assert!(
890 contains_nested_reference(&ty_option_ref),
891 "Option<&T> should be rejected"
892 );
893 assert!(
894 contains_nested_reference(&ty_vec_ref),
895 "Vec<&T> should be rejected"
896 );
897 assert!(
898 !contains_nested_reference(&ty_owned),
899 "owned generic types should be allowed"
900 );
901 }
902}