1#![doc = include_str!("../README.md")]
2
3use core::panic;
4use proc_macro::TokenStream;
5use proc_macro2::TokenStream as TokenStream2;
6use quote::{format_ident, quote};
7use std::collections::HashMap;
8use std::{fs, path::Path};
9use swc_common::comments::{CommentKind, Comments};
10use swc_common::{SourceMap, Span, comments::SingleThreadedComments};
11use swc_common::{SourceMapper, Spanned};
12use swc_ecma_ast::{
13 Decl, ExportDecl, ExportSpecifier, FnDecl, NamedExport, Pat, TsType, TsTypeAnn, VarDeclarator,
14};
15use swc_ecma_parser::EsSyntax;
16use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
17use swc_ecma_visit::{Visit, VisitWith};
18use syn::TypeParam;
19use syn::{
20 Ident, LitStr, Result, Token,
21 parse::{Parse, ParseStream},
22 parse_macro_input,
23};
24
25const JSVALUE_START: &str = "JsValue";
27const JSVALUE: &str = "dioxus_use_js::JsValue";
28const DEFAULT_GENRIC_INPUT: &str = "impl dioxus_use_js::SerdeSerialize";
29const DEFAULT_GENERIC_OUTPUT: &str = "DeserializeOwned";
30const DEFAULT_OUTPUT_GENERIC_DECLARTION: &str =
31 "DeserializeOwned: dioxus_use_js::SerdeDeDeserializeOwned";
32const SERDE_VALUE: &str = "dioxus_use_js::SerdeJsonValue";
33const JSON: &str = "Json";
34const RUST_CALLBACK_JS_START: &str = "RustCallback";
36const UNIT: &str = "()";
37
38#[derive(Debug, Clone)]
39enum ImportSpec {
40 All,
42 Named(Vec<Ident>),
44 Single(Ident),
46}
47
48struct UseJsInput {
49 js_bundle_path: LitStr,
50 ts_source_path: Option<LitStr>,
51 import_spec: ImportSpec,
52}
53
54impl Parse for UseJsInput {
55 fn parse(input: ParseStream) -> Result<Self> {
56 let first_str: LitStr = input.parse()?;
57
58 let (ts_source_path, js_bundle_path) = if input.peek(Token![,]) {
60 input.parse::<Token![,]>()?;
61 let second_str: LitStr = input.parse()?;
62 (Some(first_str), second_str)
63 } else {
64 (None, first_str)
65 };
66
67 let import_spec = if input.peek(Token![::]) {
69 input.parse::<Token![::]>()?;
70
71 if input.peek(Token![*]) {
72 input.parse::<Token![*]>()?;
73 ImportSpec::All
74 } else if input.peek(Ident) {
75 let ident: Ident = input.parse()?;
76 ImportSpec::Single(ident)
77 } else if input.peek(syn::token::Brace) {
78 let content;
79 syn::braced!(content in input);
80 let idents: syn::punctuated::Punctuated<Ident, Token![,]> =
81 content.parse_terminated(Ident::parse, Token![,])?;
82 ImportSpec::Named(idents.into_iter().collect())
83 } else {
84 return Err(input.error("Expected `*`, an identifier, or a brace group after `::`"));
85 }
86 } else {
87 return Err(input
88 .error("Expected `::` followed by an import spec (even for wildcard with `*`)"));
89 };
90
91 Ok(UseJsInput {
92 js_bundle_path,
93 ts_source_path,
94 import_spec,
95 })
96 }
97}
98
99#[derive(Debug, Clone)]
100struct ParamInfo {
101 name: String,
102 #[allow(unused)]
103 js_type: Option<String>,
104 rust_type: RustType,
105}
106
107#[derive(Debug, Clone)]
108struct FunctionInfo {
109 name: String,
110 name_ident: Option<Ident>,
112 params: Vec<ParamInfo>,
114 #[allow(unused)]
116 js_return_type: Option<String>,
117 rust_return_type: RustType,
118 is_exported: bool,
119 is_async: bool,
120 doc_comment: Vec<String>,
122}
123
124struct FunctionVisitor {
125 functions: Vec<FunctionInfo>,
126 comments: SingleThreadedComments,
127 source_map: SourceMap,
128}
129
130impl FunctionVisitor {
131 fn new(comments: SingleThreadedComments, source_map: SourceMap) -> Self {
132 Self {
133 functions: Vec::new(),
134 comments,
135 source_map,
136 }
137 }
138
139 fn extract_doc_comment(&self, span: &Span) -> Vec<String> {
140 let leading_comment = self.comments.get_leading(span.lo());
142
143 if let Some(comments) = leading_comment {
144 let mut doc_lines = Vec::new();
145
146 for comment in comments.iter() {
147 let comment_text = &comment.text;
148 match comment.kind {
149 CommentKind::Line => {
151 if let Some(content) = comment_text.strip_prefix("/") {
152 let cleaned = content.trim_start();
153 doc_lines.push(cleaned.to_string());
154 }
155 }
156 CommentKind::Block => {
158 for line in comment_text.lines() {
159 if let Some(cleaned) = line.trim_start().strip_prefix("*") {
160 doc_lines.push(cleaned.to_string());
161 }
162 }
163 }
164 };
165 }
166
167 doc_lines
168 } else {
169 Vec::new()
170 }
171 }
172}
173
174#[derive(Debug, Clone)]
175enum RustType {
176 Regular(String),
177 CallBack(RustCallback),
178 JsValue(JsValue),
179}
180
181impl ToString for RustType {
182 fn to_string(&self) -> String {
183 match self {
184 RustType::Regular(ty) => ty.clone(),
185 RustType::CallBack(callback) => callback.to_string(),
186 RustType::JsValue(js_value) => js_value.to_string(),
187 }
188 }
189}
190
191impl RustType {
192 fn to_tokens(&self) -> TokenStream2 {
193 self.to_string()
194 .parse::<TokenStream2>()
195 .expect("Calculated Rust type should always be valid")
196 }
197}
198
199#[derive(Debug, Clone)]
200struct RustCallback {
201 input: Option<String>,
202 output: Option<String>,
203}
204
205impl ToString for RustCallback {
206 fn to_string(&self) -> String {
207 let input = self.input.as_deref();
208 let output = self.output.as_deref().unwrap_or(UNIT);
209 format!(
210 "impl AsyncFnMut({}) -> Result<{}, Box<dyn std::error::Error + Send + Sync>>",
211 input.unwrap_or_default(),
212 output
213 )
214 }
215}
216
217#[derive(Debug, Clone)]
218struct JsValue {
219 is_option: bool,
220 is_input: bool,
221}
222
223impl ToString for JsValue {
224 fn to_string(&self) -> String {
225 if self.is_option {
226 format!(
227 "Option<{}>",
228 if self.is_input {
229 format!("&{}", JSVALUE)
230 } else {
231 JSVALUE.to_owned()
232 }
233 )
234 } else {
235 if self.is_input {
236 format!("&{}", JSVALUE)
237 } else {
238 JSVALUE.to_owned()
239 }
240 }
241 }
242}
243
244fn strip_parenthesis(mut ts_type: &str) -> &str {
245 while ts_type.starts_with("(") && ts_type.ends_with(")") {
246 ts_type = &ts_type[1..ts_type.len() - 1].trim();
247 }
248 return ts_type;
249}
250
251fn split_into_args(ts_type: &str) -> Vec<&str> {
253 let mut depth_angle: u16 = 0;
254 let mut depth_square: u16 = 0;
255 let mut depth_paren: u16 = 0;
256 let mut splits = Vec::new();
257 let mut last: usize = 0;
258 for (i, c) in ts_type.char_indices() {
259 match c {
260 '<' => depth_angle += 1,
261 '>' => depth_angle = depth_angle.saturating_sub(1),
262 '[' => depth_square += 1,
263 ']' => depth_square = depth_square.saturating_sub(1),
264 '(' => depth_paren += 1,
265 ')' => depth_paren = depth_paren.saturating_sub(1),
266 ',' if depth_angle == 0 && depth_square == 0 && depth_paren == 0 => {
267 splits.push(ts_type[last..i].trim());
268 last = i + 1;
269 }
270 _ => {}
271 }
272 }
273 let len = ts_type.len();
274 if last != len {
275 let maybe_arg = ts_type[last..len].trim();
276 if !maybe_arg.is_empty() {
277 splits.push(maybe_arg);
278 }
279 }
280 splits
281}
282
283fn ts_type_to_rust_type(ts_type: Option<&str>, is_input: bool) -> RustType {
284 let Some(mut ts_type) = ts_type else {
285 return RustType::Regular(
286 (if is_input {
287 DEFAULT_GENRIC_INPUT
288 } else {
289 DEFAULT_GENERIC_OUTPUT
290 })
291 .to_owned(),
292 );
293 };
294 ts_type = strip_parenthesis(&mut ts_type);
295 if ts_type.starts_with("Promise<") && ts_type.ends_with(">") {
296 assert!(!is_input, "Promise cannot be used as input type");
297 ts_type = &ts_type[8..ts_type.len() - 1];
298 }
299 ts_type = strip_parenthesis(&mut ts_type);
300 if ts_type.contains(JSVALUE_START) {
301 let parts = split_top_level_union(ts_type);
302 let len = parts.len();
303 if len == 1 && parts[0].starts_with(JSVALUE_START) {
304 return RustType::JsValue(JsValue {
305 is_option: false,
306 is_input,
307 });
308 }
309
310 if len == 2 && parts.contains(&"null") {
311 return RustType::JsValue(JsValue {
312 is_option: true,
313 is_input,
314 });
315 } else {
316 panic!("Invalid use of `{}` for `{}`", JSVALUE_START, ts_type);
317 }
318 }
319 if ts_type.contains(RUST_CALLBACK_JS_START) {
320 if !ts_type.starts_with(RUST_CALLBACK_JS_START) {
321 panic!("Nested RustCallback is not valid: {}", ts_type);
322 }
323 assert!(is_input, "Cannot return a RustCallback: {}", ts_type);
324 let ts_type = &ts_type[RUST_CALLBACK_JS_START.len()..];
325 if !(ts_type.starts_with("<") && ts_type.ends_with(">")) {
326 panic!("Invalid RustCallback type: {}", ts_type);
327 }
328 let inner = &ts_type[1..ts_type.len() - 1];
329 let parts = split_into_args(inner);
330 let len = parts.len();
331 if len != 2 {
332 panic!(
333 "A RustCallback type expects two parameters, got: {:?}",
334 parts
335 );
336 }
337 let ts_input = parts[0];
338 let rs_input = if ts_input == "void" {
339 None
340 } else {
341 let rs_input = ts_type_to_rust_type_helper(ts_input, false);
342 if rs_input.is_none() || rs_input.as_ref().is_some_and(|e| e == UNIT) {
343 panic!("Type `{ts_input}` is not a valid input for `{RUST_CALLBACK_JS_START}`");
344 }
345 rs_input
346 };
347 let ts_output = parts[1];
349 let rs_output = if ts_output == "void" {
350 None
351 } else {
352 let rs_output = ts_type_to_rust_type_helper(ts_output, false);
353 if rs_output.is_none() || rs_output.as_ref().is_some_and(|e| e == UNIT) {
354 panic!("Type `{ts_output}` is not a valid output for `{RUST_CALLBACK_JS_START}`");
355 }
356 rs_output
357 };
358 return RustType::CallBack(RustCallback {
359 input: rs_input,
360 output: rs_output,
361 });
362 }
363 RustType::Regular(match ts_type_to_rust_type_helper(ts_type, is_input) {
364 Some(value) => {
365 if value.contains(UNIT) && (is_input || &value != UNIT) {
366 panic!("`{}` is not valid in this positioh", ts_type);
369 }
370 value
371 }
372 None => (if is_input {
373 DEFAULT_GENRIC_INPUT
374 } else {
375 DEFAULT_GENERIC_OUTPUT
376 })
377 .to_owned(),
378 })
379}
380
381fn ts_type_to_rust_type_helper(mut ts_type: &str, can_be_ref: bool) -> Option<String> {
383 ts_type = ts_type.trim();
384 ts_type = strip_parenthesis(&mut ts_type);
385
386 let parts = split_top_level_union(ts_type);
387 if parts.len() > 1 {
388 if parts.len() == 2 && parts.contains(&"null") {
390 let inner = parts.iter().find(|p| **p != "null")?;
391 let inner_rust = ts_type_to_rust_type_helper(inner, can_be_ref)?;
392 return Some(format!("Option<{}>", inner_rust));
393 }
394 return None;
396 }
397
398 ts_type = parts[0];
399
400 if ts_type.ends_with("[]") {
401 let inner = ts_type.strip_suffix("[]").unwrap();
402 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
403 return Some(if can_be_ref {
404 format!("&[{}]", inner_rust)
405 } else {
406 format!("Vec<{}>", inner_rust)
407 });
408 }
409
410 if ts_type.starts_with("Array<") && ts_type.ends_with(">") {
411 let inner = &ts_type[6..ts_type.len() - 1];
412 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
413 return Some(if can_be_ref {
414 format!("&[{}]", inner_rust)
415 } else {
416 format!("Vec<{}>", inner_rust)
417 });
418 }
419
420 if ts_type.starts_with("Set<") && ts_type.ends_with(">") {
421 let inner = &ts_type[4..ts_type.len() - 1];
422 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
423 if can_be_ref {
424 return Some(format!("&std::collections::HashSet<{}>", inner_rust));
425 } else {
426 return Some(format!("std::collections::HashSet<{}>", inner_rust));
427 }
428 }
429
430 if ts_type.starts_with("Map<") && ts_type.ends_with(">") {
431 let inner = &ts_type[4..ts_type.len() - 1];
432 let mut depth = 0;
433 let mut split_index = None;
434 for (i, c) in inner.char_indices() {
435 match c {
436 '<' => depth += 1,
437 '>' => depth -= 1,
438 ',' if depth == 0 => {
439 split_index = Some(i);
440 break;
441 }
442 _ => {}
443 }
444 }
445
446 if let Some(i) = split_index {
447 let (key, value) = inner.split_at(i);
448 let value = &value[1..]; let key_rust = ts_type_to_rust_type_helper(key.trim(), false)?;
450 let value_rust = ts_type_to_rust_type_helper(value.trim(), false)?;
451 if can_be_ref {
452 return Some(format!(
453 "&std::collections::HashMap<{}, {}>",
454 key_rust, value_rust
455 ));
456 } else {
457 return Some(format!(
458 "std::collections::HashMap<{}, {}>",
459 key_rust, value_rust
460 ));
461 }
462 } else {
463 return None;
464 }
465 }
466
467 let rust_type = match ts_type {
469 "string" => {
470 if can_be_ref {
471 Some("&str".to_owned())
472 } else {
473 Some("String".to_owned())
474 }
475 }
476 "number" => Some("f64".to_owned()),
477 "boolean" => Some("bool".to_owned()),
478 "void" | "undefined" | "never" | "null" => Some(UNIT.to_owned()),
479 JSON => {
480 if can_be_ref {
481 Some(format!("&{SERDE_VALUE}"))
482 } else {
483 Some(SERDE_VALUE.to_owned())
484 }
485 }
486 "Promise" => {
487 panic!("`{}` - nested promises are not valid", ts_type)
488 }
489 _ => None,
491 };
492
493 rust_type
494}
495
496fn split_top_level_union(s: &str) -> Vec<&str> {
498 let mut parts = vec![];
499 let mut last = 0;
500 let mut depth_angle = 0;
501 let mut depth_paren = 0;
502
503 for (i, c) in s.char_indices() {
504 match c {
505 '<' => depth_angle += 1,
506 '>' => {
507 if depth_angle > 0 {
508 depth_angle -= 1
509 }
510 }
511 '(' => depth_paren += 1,
512 ')' => {
513 if depth_paren > 0 {
514 depth_paren -= 1
515 }
516 }
517 '|' if depth_angle == 0 && depth_paren == 0 => {
518 parts.push(s[last..i].trim());
519 last = i + 1;
520 }
521 _ => {}
522 }
523 }
524
525 if last < s.len() {
526 parts.push(s[last..].trim());
527 }
528
529 parts
530}
531
532fn type_to_string(ty: &Box<TsType>, source_map: &SourceMap) -> String {
533 let span = ty.span();
534 source_map
535 .span_to_snippet(span)
536 .expect("Could not get snippet from span for type")
537}
538
539fn function_pat_to_param_info<'a, I>(pats: I, source_map: &SourceMap) -> Vec<ParamInfo>
540where
541 I: Iterator<Item = &'a Pat>,
542{
543 pats.enumerate()
544 .map(|(i, pat)| to_param_info_helper(i, pat, source_map))
545 .collect()
546}
547
548fn to_param_info_helper(i: usize, pat: &Pat, source_map: &SourceMap) -> ParamInfo {
549 let name = if let Some(ident) = pat.as_ident() {
550 ident.id.sym.to_string()
551 } else {
552 format!("arg{}", i)
553 };
554
555 let js_type = pat
556 .as_ident()
557 .and_then(|ident| ident.type_ann.as_ref())
558 .map(|type_ann| {
559 let ty = &type_ann.type_ann;
560 type_to_string(ty, source_map)
561 });
562 let rust_type = ts_type_to_rust_type(js_type.as_deref(), true);
563
564 ParamInfo {
565 name,
566 js_type,
567 rust_type,
568 }
569}
570
571fn function_info_helper<'a, I>(
572 visitor: &FunctionVisitor,
573 name: String,
574 span: &Span,
575 params: I,
576 return_type: Option<&Box<TsTypeAnn>>,
577 is_async: bool,
578 is_exported: bool,
579) -> FunctionInfo
580where
581 I: Iterator<Item = &'a Pat>,
582{
583 let doc_comment = visitor.extract_doc_comment(span);
584
585 let params = function_pat_to_param_info(params, &visitor.source_map);
586
587 let js_return_type = return_type.as_ref().map(|type_ann| {
588 let ty = &type_ann.type_ann;
589 type_to_string(ty, &visitor.source_map)
590 });
591 if !is_async
592 && let Some(ref js_return_type) = js_return_type
593 && js_return_type.starts_with("Promise")
594 {
595 panic!(
596 "Promise return type is only supported for async functions, use `async fn` instead. For `{js_return_type}`"
597 );
598 }
599
600 let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
601
602 FunctionInfo {
603 name,
604 name_ident: None,
605 params,
606 js_return_type,
607 rust_return_type,
608 is_exported,
609 is_async,
610 doc_comment,
611 }
612}
613
614impl Visit for FunctionVisitor {
615 fn visit_fn_decl(&mut self, node: &FnDecl) {
617 let name = node.ident.sym.to_string();
618 self.functions.push(function_info_helper(
619 self,
620 name,
621 &node.span(),
622 node.function.params.iter().map(|e| &e.pat),
623 node.function.return_type.as_ref(),
624 node.function.is_async,
625 false,
626 ));
627 node.visit_children_with(self);
628 }
629
630 fn visit_var_declarator(&mut self, node: &VarDeclarator) {
632 if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
633 if let Some(init) = &node.init {
634 let span = node.span();
635 let name = ident.id.sym.to_string();
636 match &**init {
637 swc_ecma_ast::Expr::Fn(fn_expr) => {
638 self.functions.push(function_info_helper(
639 &self,
640 name,
641 &span,
642 fn_expr.function.params.iter().map(|e| &e.pat),
643 fn_expr.function.return_type.as_ref(),
644 fn_expr.function.is_async,
645 false,
646 ));
647 }
648 swc_ecma_ast::Expr::Arrow(arrow_fn) => {
649 self.functions.push(function_info_helper(
650 &self,
651 name,
652 &span,
653 arrow_fn.params.iter(),
654 arrow_fn.return_type.as_ref(),
655 arrow_fn.is_async,
656 false,
657 ));
658 }
659 _ => {}
660 }
661 }
662 }
663 node.visit_children_with(self);
664 }
665
666 fn visit_export_decl(&mut self, node: &ExportDecl) {
668 if let Decl::Fn(fn_decl) = &node.decl {
669 let span = node.span();
670 let name = fn_decl.ident.sym.to_string();
671 self.functions.push(function_info_helper(
672 &self,
673 name,
674 &span,
675 fn_decl.function.params.iter().map(|e| &e.pat),
676 fn_decl.function.return_type.as_ref(),
677 fn_decl.function.is_async,
678 true,
679 ));
680 }
681 node.visit_children_with(self);
682 }
683
684 fn visit_named_export(&mut self, node: &NamedExport) {
686 for spec in &node.specifiers {
687 if let ExportSpecifier::Named(named) = spec {
688 let original_name = named.orig.atom().to_string();
689 let out_name = named
690 .exported
691 .as_ref()
692 .map(|e| e.atom().to_string())
693 .unwrap_or_else(|| original_name.clone());
694
695 if let Some(func) = self.functions.iter_mut().find(|f| f.name == original_name) {
696 let mut func = func.clone();
697 func.name = out_name;
698 func.is_exported = true;
699 self.functions.push(func);
700 }
701 }
702 }
703 node.visit_children_with(self);
704 }
705}
706
707fn parse_script_file(file_path: &Path, is_js: bool) -> Result<Vec<FunctionInfo>> {
708 let js_content = fs::read_to_string(file_path).map_err(|e| {
709 syn::Error::new(
710 proc_macro2::Span::call_site(),
711 format!("Could not read file '{}': {}", file_path.display(), e),
712 )
713 })?;
714
715 let source_map = SourceMap::default();
716 let fm = source_map.new_source_file(
717 swc_common::FileName::Custom(file_path.display().to_string()).into(),
718 js_content.clone(),
719 );
720 let comments = SingleThreadedComments::default();
721
722 let syntax = if is_js {
724 Syntax::Es(EsSyntax {
725 jsx: false,
726 fn_bind: false,
727 decorators: false,
728 decorators_before_export: false,
729 export_default_from: false,
730 import_attributes: false,
731 allow_super_outside_method: false,
732 allow_return_outside_function: false,
733 auto_accessors: false,
734 explicit_resource_management: false,
735 })
736 } else {
737 Syntax::Typescript(swc_ecma_parser::TsSyntax {
738 tsx: false,
739 decorators: false,
740 dts: false,
741 no_early_errors: false,
742 disallow_ambiguous_jsx_like: true,
743 })
744 };
745
746 let lexer = Lexer::new(
747 syntax,
748 Default::default(),
749 StringInput::from(&*fm),
750 Some(&comments),
751 );
752
753 let mut parser = Parser::new_from(lexer);
754
755 let module = parser.parse_module().map_err(|e| {
756 syn::Error::new(
757 proc_macro2::Span::call_site(),
758 format!(
759 "Failed to parse script file '{}': {:?}",
760 file_path.display(),
761 e
762 ),
763 )
764 })?;
765
766 let mut visitor = FunctionVisitor::new(comments, source_map);
767 module.visit_with(&mut visitor);
768
769 visitor
771 .functions
772 .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
773 Ok(visitor.functions)
774}
775
776fn take_function_by_name(
777 name: &str,
778 functions: &mut Vec<FunctionInfo>,
779 file: &Path,
780) -> Result<FunctionInfo> {
781 let function_info = if let Some(pos) = functions.iter().position(|f| f.name == name) {
782 functions.remove(pos)
783 } else {
784 return Err(syn::Error::new(
785 proc_macro2::Span::call_site(),
786 format!("Function '{}' not found in file '{}'", name, file.display()),
787 ));
788 };
789 if !function_info.is_exported {
790 return Err(syn::Error::new(
791 proc_macro2::Span::call_site(),
792 format!(
793 "Function '{}' not exported in file '{}'",
794 name,
795 file.display()
796 ),
797 ));
798 }
799 Ok(function_info)
800}
801
802fn get_functions_to_generate(
803 mut functions: Vec<FunctionInfo>,
804 import_spec: &ImportSpec,
805 file: &Path,
806) -> Result<Vec<FunctionInfo>> {
807 match import_spec {
808 ImportSpec::All => Ok(functions.into_iter().filter(|e| e.is_exported).collect()),
809 ImportSpec::Single(name) => {
810 let mut func = take_function_by_name(name.to_string().as_str(), &mut functions, file)?;
811 func.name_ident.replace(name.clone());
812 Ok(vec![func])
813 }
814 ImportSpec::Named(names) => {
815 let mut result = Vec::new();
816 for name in names {
817 let mut func =
818 take_function_by_name(name.to_string().as_str(), &mut functions, file)?;
819 func.name_ident.replace(name.clone());
820 result.push(func);
821 }
822 Ok(result)
823 }
824 }
825}
826
827fn generate_function_wrapper(func: &FunctionInfo, asset_path: &LitStr) -> TokenStream2 {
828 let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
830 let mut callback_name_to_info: HashMap<String, &RustCallback> = HashMap::new();
831 let mut index: u64 = 1; for param in &func.params {
833 if let RustType::CallBack(callback) = ¶m.rust_type {
834 callback_name_to_index.insert(param.name.to_owned(), index);
835 index += 1;
836 callback_name_to_info.insert(param.name.to_owned(), callback);
837 }
838 }
839
840 let send_calls: Vec<TokenStream2> = func
841 .params
842 .iter()
843 .flat_map(|param| {
844 let param_name = format_ident!("{}", param.name);
845 match ¶m.rust_type {
846 RustType::Regular(_) => Some(quote! {
847 eval.send(#param_name).map_err(dioxus_use_js::JsError::Eval)?;
848 }),
849 RustType::JsValue(js_value) => {
850 if js_value.is_option {
851 Some(quote! {
852 #[allow(deprecated)]
853 eval.send(#param_name.map(|e| e.internal_get())).map_err(dioxus_use_js::JsError::Eval)?;
854 })
855 } else {
856 Some(quote! {
857 #[allow(deprecated)]
858 eval.send(#param_name.internal_get()).map_err(dioxus_use_js::JsError::Eval)?;
859 })
860 }
861 },
862 RustType::CallBack(_) => None,
863 }
864 })
865 .collect();
866
867 let js_func_name = &func.name;
868 let params_list = func
869 .params
870 .iter()
871 .map(|p| p.name.as_str())
872 .collect::<Vec<&str>>()
873 .join(", ");
874 let param_declaration_lines = func
875 .params
876 .iter()
877 .map(|param| match ¶m.rust_type {
878 RustType::Regular(_) => {
879 format!("let {} = await dioxus.recv();", param.name)
880 }
881 RustType::JsValue(js_value) => {
882 let param_name = ¶m.name;
883 if js_value.is_option {
884 format!(
885 "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = null;\nif ({param_name}Temp_ !== null) {{{{ {param_name} = window[{param_name}Temp_] }}}};",
886 )
887 }
888 else {
889 format!(
890 "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = window[{param_name}Temp_];",
891 )
892 }
893 },
894 RustType::CallBack(rust_callback) => {
895 let name = ¶m.name;
896 let index = callback_name_to_index.get(name).unwrap();
897 let RustCallback { input, output } = rust_callback;
898 match (input, output) {
899 (None, None) => {
900 format!(
902 "const {} = async () => {{{{ dioxus.send([{}, null]); await dioxus.recv(); }}}};",
903 name, index
904 )
905 },
906 (None, Some(_)) => {
907 format!(
908 "const {} = async () => {{{{ dioxus.send([{}, null]); return await dioxus.recv(); }}}};",
909 name, index
910
911 )
912 },
913 (Some(_), None) => {
914 format!(
916 "const {} = async (value) => {{{{ dioxus.send([{}, value]); await dioxus.recv(); }}}};",
917 name, index
918 )
919 },
920 (Some(_), Some(_)) => {
921 format!(
922 "const {} = async (value) => {{{{ dioxus.send([{}, value]); return await dioxus.recv(); }}}};",
923 name, index
924 )
925 },
926 }
927 },
928 })
929 .collect::<Vec<_>>()
930 .join("\n");
931 let mut await_fn = String::new();
932 if func.is_async {
933 await_fn.push_str("await");
934 }
935 let call_function = match &func.rust_return_type {
936 RustType::Regular(_) => {
937 format!(
939 r#"
940___result___ = {await_fn} {js_func_name}({params_list});
941"#
942 )
943 }
944 RustType::CallBack(_) => panic!("Cannot be an output type, should have panicked earlier."),
945 RustType::JsValue(js_value) => {
946 let check = if js_value.is_option {
947 "if (___resultValue___ === null || ___resultValue___ === undefined) {{{{ return null; }}}}".to_owned()
949 } else {
950 format!(
951 "if (___resultValue___ === undefined) {{{{ console.error(\"`{js_func_name}` was undefined, but value is needed for JsValue\"); return null; }}}}"
952 )
953 };
954 format!(
955 r#"
956const ___resultValue___ = {await_fn} {js_func_name}({params_list});
957{check}
958___result___ = "js-value-{js_func_name}-" + crypto.randomUUID();
959window[___result___] = ___resultValue___;
960 "#
961 )
962 }
963 };
964 let end_statement = if callback_name_to_index.is_empty() {
965 "if (___result___ === undefined) {{ return null; }}; return ___result___;"
966 } else {
967 "if (___result___ === undefined) {{ dioxus.send([0, null]); }}; dioxus.send([0, ___result___]);"
968 };
969
970 let js_format = format!(
971 r#"
972const {{{{ {js_func_name} }}}} = await import("{{}}");
973{param_declaration_lines}
974let ___result___;
975try {{{{
976{call_function}
977}}}}
978catch (e) {{{{
979console.error("Executing function `{js_func_name}` threw an error:", e);
980___result___ = undefined;
981}}}}
982{end_statement}
983"#
984 );
985
986 let param_types: Vec<_> = func
988 .params
989 .iter()
990 .map(|param| {
991 let param_name = format_ident!("{}", param.name);
992 let type_tokens = param.rust_type.to_tokens();
993 if let RustType::CallBack(_) = param.rust_type {
994 quote! { mut #param_name: #type_tokens }
995 } else {
996 quote! { #param_name: #type_tokens }
997 }
998 })
999 .collect();
1000
1001 let parsed_type = func.rust_return_type.to_tokens();
1002 let (return_type_tokens, generic_tokens) = if func.rust_return_type.to_string()
1003 == DEFAULT_GENERIC_OUTPUT
1004 {
1005 let span = func
1006 .name_ident
1007 .as_ref()
1008 .map(|e| e.span())
1009 .unwrap_or_else(|| proc_macro2::Span::call_site());
1010 let generic = Ident::new(DEFAULT_GENERIC_OUTPUT, span);
1011 let generic_decl: TypeParam = syn::parse_str(DEFAULT_OUTPUT_GENERIC_DECLARTION).unwrap();
1012 (
1013 quote! { Result<#generic, dioxus_use_js::JsError> },
1014 Some(quote! { <#generic_decl> }),
1015 )
1016 } else {
1017 (
1018 quote! { Result<#parsed_type, dioxus_use_js::JsError> },
1019 None,
1020 )
1021 };
1022
1023 let doc_comment = if func.doc_comment.is_empty() {
1025 quote! {}
1026 } else {
1027 let doc_lines: Vec<_> = func
1028 .doc_comment
1029 .iter()
1030 .map(|line| quote! { #[doc = #line] })
1031 .collect();
1032 quote! { #(#doc_lines)* }
1033 };
1034
1035 let func_name = func
1036 .name_ident
1037 .clone()
1038 .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
1040
1041 let void_output_mapping = if func.rust_return_type.to_string() == UNIT {
1043 quote! {
1044 .and_then(|e| {
1045 if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
1046 Ok(())
1047 } else {
1048 Err(dioxus_use_js::JsError::Eval(
1049 dioxus::document::EvalError::Serialization(
1050 <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1051 )
1052 ))
1053 }
1054 })
1055 }
1056 } else {
1057 quote! {}
1058 };
1059
1060 let has_no_callbacks = callback_name_to_index.is_empty();
1061 let end_statement = if has_no_callbacks {
1062 let return_value_mapping = if func.rust_return_type.to_string() == SERDE_VALUE {
1063 quote! {
1064 .map_err(dioxus_use_js::JsError::Eval)
1065 }
1066 } else {
1067 quote! {
1068 .map_err(dioxus_use_js::JsError::Eval)
1069 .and_then(|v| dioxus_use_js::serde_json_from_value(v).map_err(|e| dioxus_use_js::JsError::Eval(dioxus::document::EvalError::Serialization(e))))
1070 }
1071 };
1072
1073 match &func.rust_return_type {
1074 RustType::Regular(_) => {
1075 quote! {
1076 eval
1077 .await
1078 #return_value_mapping
1079 #void_output_mapping
1080 }
1081 }
1082 RustType::CallBack(_) => {
1083 panic!("Cannot be an output type, should have panicked earlier.")
1084 }
1085 RustType::JsValue(js_value) => {
1086 if js_value.is_option {
1087 quote! {
1088 let id: Option<String> = eval
1089 .await
1090 #return_value_mapping?;
1091 #[allow(deprecated)]
1092 Ok(id.map(|e| dioxus_use_js::JsValue::internal_create(e)))
1093 }
1094 } else {
1095 quote! {
1096 let id: String = eval
1097 .await
1098 #return_value_mapping?;
1099 #[allow(deprecated)]
1100 Ok(dioxus_use_js::JsValue::internal_create(id))
1101 }
1102 }
1103 }
1104 }
1105 } else {
1106 let callback_arms: Vec<TokenStream2> = callback_name_to_index
1107 .iter()
1108 .map(|(name, index)| {
1109 let callback = callback_name_to_info.get(name).unwrap();
1110 let callback_call = if let Some(_) = callback.input {
1111 quote! {
1112 let value = dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1113 dioxus_use_js::JsError::Eval(
1114 dioxus::document::EvalError::Serialization(e),
1115 )
1116 })?;
1117 let value = match callback(value).await {
1118 Ok(value) => value,
1119 Err(error) => {
1120 return Err(dioxus_use_js::JsError::Callback(error));
1121 }
1122 };
1123 }
1124 } else {
1125 quote! {
1126 let value = match callback().await {
1127 Ok(value) => value,
1128 Err(error) => {
1129 return Err(dioxus_use_js::JsError::Callback(error));
1130 }
1131 };
1132 }
1133 };
1134
1135 let callback_send_back = if let Some(_) = callback.output {
1136 quote! {
1137 eval.send(value).map_err(dioxus_use_js::JsError::Eval)?;
1138 }
1139 } else {
1140 quote! {
1142 eval.send(dioxus_use_js::SerdeJsonValue::Null).map_err(dioxus_use_js::JsError::Eval)?;
1143 }
1144 };
1145 quote! {
1146 #index => {
1147 #callback_call
1148 #callback_send_back
1149 }
1150 }
1151 })
1152 .collect();
1153
1154 quote! {
1155 loop {
1156 let value = eval
1157 .recv::<dioxus_use_js::SerdeJsonValue>()
1158 .await
1159 .map_err(dioxus_use_js::JsError::Eval)?;
1160 match value{
1161 dioxus_use_js::SerdeJsonValue::Array(values) => {
1162 if values.len() != 2 {
1163 unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG)
1164 }
1165 let mut iter = values.into_iter();
1166 let action_ = match iter.next().unwrap() {
1167 dioxus_use_js::SerdeJsonValue::Number(action_) => action_,
1168 _ => unreachable!("{}", dioxus_use_js::__INDEX_VALIDATION_MSG),
1169 };
1170 let value = iter.next().unwrap();
1171 match action_.as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG) {
1172 0 => {
1173 return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1174 dioxus_use_js::JsError::Eval(
1175 dioxus::document::EvalError::Serialization(e),
1176 )
1177 })
1178 #void_output_mapping;
1179 }
1180 #(#callback_arms,)*
1181 _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1182 }
1183 }
1184 _ => unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG),
1185 }
1186 }
1187 }
1188 };
1189
1190 quote! {
1191 #doc_comment
1192 #[allow(non_snake_case)]
1193 pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1194 const MODULE: Asset = asset!(#asset_path);
1195 let js = format!(#js_format, MODULE);
1196 let mut eval = dioxus::document::eval(js.as_str());
1197 #(#send_calls)*
1198 #end_statement
1199 }
1200 }
1201}
1202
1203#[proc_macro]
1205pub fn use_js(input: TokenStream) -> TokenStream {
1206 let input = parse_macro_input!(input as UseJsInput);
1207
1208 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1209 Ok(dir) => dir,
1210 Err(_) => {
1211 return TokenStream::from(
1212 syn::Error::new(
1213 proc_macro2::Span::call_site(),
1214 "CARGO_MANIFEST_DIR environment variable not found",
1215 )
1216 .to_compile_error(),
1217 );
1218 }
1219 };
1220
1221 let UseJsInput {
1222 js_bundle_path,
1223 ts_source_path,
1224 import_spec,
1225 } = input;
1226
1227 let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1228
1229 let js_all_functions = match parse_script_file(&js_file_path, true) {
1230 Ok(funcs) => funcs,
1231 Err(e) => return TokenStream::from(e.to_compile_error()),
1232 };
1233
1234 let js_functions_to_generate =
1235 match get_functions_to_generate(js_all_functions, &import_spec, &js_file_path) {
1236 Ok(funcs) => funcs,
1237 Err(e) => return TokenStream::from(e.to_compile_error()),
1238 };
1239
1240 let functions_to_generate = if let Some(ts_file_path) = ts_source_path {
1241 let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1242 let ts_all_functions = match parse_script_file(&ts_file_path, false) {
1243 Ok(funcs) => funcs,
1244 Err(e) => return TokenStream::from(e.to_compile_error()),
1245 };
1246
1247 let ts_functions_to_generate =
1248 match get_functions_to_generate(ts_all_functions, &import_spec, &ts_file_path) {
1249 Ok(funcs) => funcs,
1250 Err(e) => {
1251 return TokenStream::from(e.to_compile_error());
1252 }
1253 };
1254
1255 for ts_func in ts_functions_to_generate.iter() {
1256 if let Some(js_func) = js_functions_to_generate
1257 .iter()
1258 .find(|f| f.name == ts_func.name)
1259 {
1260 if ts_func.params.len() != js_func.params.len() {
1261 return TokenStream::from(syn::Error::new(
1262 proc_macro2::Span::call_site(),
1263 format!(
1264 "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1265 ts_func.name
1266 ),
1267 )
1268 .to_compile_error());
1269 }
1270 } else {
1271 return TokenStream::from(syn::Error::new(
1272 proc_macro2::Span::call_site(),
1273 format!(
1274 "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1275 ts_func.name
1276 ),
1277 )
1278 .to_compile_error());
1279 }
1280 }
1281 ts_functions_to_generate
1282 } else {
1283 js_functions_to_generate
1284 };
1285
1286 let function_wrappers: Vec<TokenStream2> = functions_to_generate
1287 .iter()
1288 .map(|func| generate_function_wrapper(func, &js_bundle_path))
1289 .collect();
1290
1291 let expanded = quote! {
1292 #(#function_wrappers)*
1293 };
1294
1295 TokenStream::from(expanded)
1296}
1297
1298#[cfg(test)]
1301mod tests {
1302 use super::*;
1303
1304 #[test]
1305 fn test_primitives() {
1306 assert_eq!(
1307 ts_type_to_rust_type(Some("string"), false).to_string(),
1308 "String"
1309 );
1310 assert_eq!(
1311 ts_type_to_rust_type(Some("string"), true).to_string(),
1312 "&str"
1313 );
1314 assert_eq!(
1315 ts_type_to_rust_type(Some("number"), false).to_string(),
1316 "f64"
1317 );
1318 assert_eq!(
1319 ts_type_to_rust_type(Some("number"), true).to_string(),
1320 "f64"
1321 );
1322 assert_eq!(
1323 ts_type_to_rust_type(Some("boolean"), false).to_string(),
1324 "bool"
1325 );
1326 assert_eq!(
1327 ts_type_to_rust_type(Some("boolean"), true).to_string(),
1328 "bool"
1329 );
1330 }
1331
1332 #[test]
1333 fn test_nullable_primitives() {
1334 assert_eq!(
1335 ts_type_to_rust_type(Some("string | null"), true).to_string(),
1336 "Option<&str>"
1337 );
1338 assert_eq!(
1339 ts_type_to_rust_type(Some("string | null"), false).to_string(),
1340 "Option<String>"
1341 );
1342 assert_eq!(
1343 ts_type_to_rust_type(Some("number | null"), true).to_string(),
1344 "Option<f64>"
1345 );
1346 assert_eq!(
1347 ts_type_to_rust_type(Some("number | null"), false).to_string(),
1348 "Option<f64>"
1349 );
1350 assert_eq!(
1351 ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1352 "Option<bool>"
1353 );
1354 assert_eq!(
1355 ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1356 "Option<bool>"
1357 );
1358 }
1359
1360 #[test]
1361 fn test_arrays() {
1362 assert_eq!(
1363 ts_type_to_rust_type(Some("string[]"), true).to_string(),
1364 "&[String]"
1365 );
1366 assert_eq!(
1367 ts_type_to_rust_type(Some("string[]"), false).to_string(),
1368 "Vec<String>"
1369 );
1370 assert_eq!(
1371 ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
1372 "&[f64]"
1373 );
1374 assert_eq!(
1375 ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
1376 "Vec<f64>"
1377 );
1378 }
1379
1380 #[test]
1381 fn test_nullable_array_elements() {
1382 assert_eq!(
1383 ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
1384 "&[Option<String>]"
1385 );
1386 assert_eq!(
1387 ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
1388 "Vec<Option<String>>"
1389 );
1390 assert_eq!(
1391 ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
1392 "&[Option<f64>]"
1393 );
1394 assert_eq!(
1395 ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
1396 "Vec<Option<f64>>"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_nullable_array_itself() {
1402 assert_eq!(
1403 ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
1404 "Option<&[String]>"
1405 );
1406 assert_eq!(
1407 ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
1408 "Option<Vec<String>>"
1409 );
1410 assert_eq!(
1411 ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
1412 "Option<&[f64]>"
1413 );
1414 assert_eq!(
1415 ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
1416 "Option<Vec<f64>>"
1417 );
1418 }
1419
1420 #[test]
1421 fn test_nullable_array_and_elements() {
1422 assert_eq!(
1423 ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
1424 "Option<&[Option<String>]>"
1425 );
1426 assert_eq!(
1427 ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
1428 "Option<Vec<Option<String>>>"
1429 );
1430 }
1431
1432 #[test]
1433 fn test_fallback_for_union() {
1434 assert_eq!(
1435 ts_type_to_rust_type(Some("string | number"), true).to_string(),
1436 "impl dioxus_use_js::SerdeSerialize"
1437 );
1438 assert_eq!(
1439 ts_type_to_rust_type(Some("string | number"), false).to_string(),
1440 "DeserializeOwned"
1441 );
1442 assert_eq!(
1443 ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
1444 "impl dioxus_use_js::SerdeSerialize"
1445 );
1446 assert_eq!(
1447 ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
1448 "DeserializeOwned"
1449 );
1450 }
1451
1452 #[test]
1453 fn test_unknown_types() {
1454 assert_eq!(
1455 ts_type_to_rust_type(Some("foo"), true).to_string(),
1456 "impl dioxus_use_js::SerdeSerialize"
1457 );
1458 assert_eq!(
1459 ts_type_to_rust_type(Some("foo"), false).to_string(),
1460 "DeserializeOwned"
1461 );
1462
1463 assert_eq!(
1464 ts_type_to_rust_type(Some("any"), true).to_string(),
1465 "impl dioxus_use_js::SerdeSerialize"
1466 );
1467 assert_eq!(
1468 ts_type_to_rust_type(Some("any"), false).to_string(),
1469 "DeserializeOwned"
1470 );
1471 assert_eq!(
1472 ts_type_to_rust_type(Some("object"), true).to_string(),
1473 "impl dioxus_use_js::SerdeSerialize"
1474 );
1475 assert_eq!(
1476 ts_type_to_rust_type(Some("object"), false).to_string(),
1477 "DeserializeOwned"
1478 );
1479 assert_eq!(
1480 ts_type_to_rust_type(Some("unknown"), true).to_string(),
1481 "impl dioxus_use_js::SerdeSerialize"
1482 );
1483 assert_eq!(
1484 ts_type_to_rust_type(Some("unknown"), false).to_string(),
1485 "DeserializeOwned"
1486 );
1487
1488 assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
1489 assert_eq!(
1490 ts_type_to_rust_type(Some("undefined"), false).to_string(),
1491 "()"
1492 );
1493 assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
1494 }
1495
1496 #[test]
1497 fn test_extra_whitespace() {
1498 assert_eq!(
1499 ts_type_to_rust_type(Some(" string | null "), true).to_string(),
1500 "Option<&str>"
1501 );
1502 assert_eq!(
1503 ts_type_to_rust_type(Some(" string | null "), false).to_string(),
1504 "Option<String>"
1505 );
1506 assert_eq!(
1507 ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
1508 "&[String]"
1509 );
1510 assert_eq!(
1511 ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
1512 "Vec<String>"
1513 );
1514 }
1515
1516 #[test]
1517 fn test_map_types() {
1518 assert_eq!(
1519 ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
1520 "&std::collections::HashMap<String, f64>"
1521 );
1522 assert_eq!(
1523 ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
1524 "std::collections::HashMap<String, f64>"
1525 );
1526 assert_eq!(
1527 ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
1528 "&std::collections::HashMap<String, bool>"
1529 );
1530 assert_eq!(
1531 ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
1532 "std::collections::HashMap<String, bool>"
1533 );
1534 assert_eq!(
1535 ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
1536 "&std::collections::HashMap<f64, String>"
1537 );
1538 assert_eq!(
1539 ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
1540 "std::collections::HashMap<f64, String>"
1541 );
1542 }
1543
1544 #[test]
1545 fn test_set_types() {
1546 assert_eq!(
1547 ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
1548 "&std::collections::HashSet<String>"
1549 );
1550 assert_eq!(
1551 ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
1552 "std::collections::HashSet<String>"
1553 );
1554 assert_eq!(
1555 ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
1556 "&std::collections::HashSet<f64>"
1557 );
1558 assert_eq!(
1559 ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
1560 "std::collections::HashSet<f64>"
1561 );
1562 assert_eq!(
1563 ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
1564 "&std::collections::HashSet<bool>"
1565 );
1566 assert_eq!(
1567 ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
1568 "std::collections::HashSet<bool>"
1569 );
1570 }
1571
1572 #[test]
1573 fn test_rust_callback() {
1574 assert_eq!(
1575 ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
1576 "impl AsyncFnMut(f64) -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1577 );
1578 assert_eq!(
1579 ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
1580 "impl AsyncFnMut() -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1581 );
1582 assert_eq!(
1583 ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
1584 "impl AsyncFnMut() -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1585 );
1586 assert_eq!(
1587 ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
1588 "impl AsyncFnMut(f64) -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1589 );
1590 }
1591
1592 #[test]
1593 fn test_promise_types() {
1594 assert_eq!(
1595 ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
1596 "String"
1597 );
1598 assert_eq!(
1599 ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
1600 "f64"
1601 );
1602 assert_eq!(
1603 ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
1604 "bool"
1605 );
1606 }
1607
1608 #[test]
1609 fn test_json_types() {
1610 assert_eq!(
1611 ts_type_to_rust_type(Some("Json"), true).to_string(),
1612 "&dioxus_use_js::SerdeJsonValue"
1613 );
1614 assert_eq!(
1615 ts_type_to_rust_type(Some("Json"), false).to_string(),
1616 "dioxus_use_js::SerdeJsonValue"
1617 );
1618 }
1619
1620 #[test]
1621 fn test_js_value() {
1622 assert_eq!(
1623 ts_type_to_rust_type(Some("JsValue"), true).to_string(),
1624 "&dioxus_use_js::JsValue"
1625 );
1626 assert_eq!(
1627 ts_type_to_rust_type(Some("JsValue"), false).to_string(),
1628 "dioxus_use_js::JsValue"
1629 );
1630 assert_eq!(
1631 ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
1632 "&dioxus_use_js::JsValue"
1633 );
1634 assert_eq!(
1635 ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
1636 "dioxus_use_js::JsValue"
1637 );
1638
1639 assert_eq!(
1640 ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
1641 "dioxus_use_js::JsValue"
1642 );
1643
1644 assert_eq!(
1645 ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
1646 "Option<dioxus_use_js::JsValue>"
1647 );
1648 assert_eq!(
1649 ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
1650 "Option<&dioxus_use_js::JsValue>"
1651 );
1652 assert_eq!(
1653 ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
1654 "Option<dioxus_use_js::JsValue>"
1655 );
1656 }
1657}