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