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