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 take_function_by_name(
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.into_iter().filter(|e| e.is_exported).collect()),
768 ImportSpec::Single(name) => {
769 let mut func = take_function_by_name(name.to_string().as_str(), &mut functions, file)?;
770 func.name_ident.replace(name.clone());
771 Ok(vec![func])
772 }
773 ImportSpec::Named(names) => {
774 let mut result = Vec::new();
775 for name in names {
776 let mut func =
777 take_function_by_name(name.to_string().as_str(), &mut functions, file)?;
778 func.name_ident.replace(name.clone());
779 result.push(func);
780 }
781 Ok(result)
782 }
783 }
784}
785
786fn generate_function_wrapper(func: &FunctionInfo, asset_path: &LitStr) -> TokenStream2 {
787 let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
789 let mut callback_name_to_info: HashMap<String, &RustCallback> = HashMap::new();
790 let mut index: u64 = 1; for param in &func.params {
792 if let RustType::CallBack(callback) = ¶m.rust_type {
793 callback_name_to_index.insert(param.name.to_owned(), index);
794 index += 1;
795 callback_name_to_info.insert(param.name.to_owned(), callback);
796 }
797 }
798
799 let send_calls: Vec<TokenStream2> = func
800 .params
801 .iter()
802 .flat_map(|param| {
803 let param_name = format_ident!("{}", param.name);
804 match ¶m.rust_type {
805 RustType::Regular(_) => Some(quote! {
806 eval.send(#param_name).map_err(dioxus_use_js::JsError::Eval)?;
807 }),
808 RustType::JsValue(js_value) => {
809 if js_value.is_option {
810 Some(quote! {
811 #[allow(deprecated)]
812 eval.send(#param_name.map(|e| e.internal_get())).map_err(dioxus_use_js::JsError::Eval)?;
813 })
814 } else {
815 Some(quote! {
816 #[allow(deprecated)]
817 eval.send(#param_name.internal_get()).map_err(dioxus_use_js::JsError::Eval)?;
818 })
819 }
820 },
821 RustType::CallBack(_) => None,
822 }
823 })
824 .collect();
825
826 let js_func_name = &func.name;
827 let params_list = func
828 .params
829 .iter()
830 .map(|p| p.name.as_str())
831 .collect::<Vec<&str>>()
832 .join(", ");
833 let param_declaration_lines = func
834 .params
835 .iter()
836 .map(|param| match ¶m.rust_type {
837 RustType::Regular(_) => {
838 format!("let {} = await dioxus.recv();", param.name)
839 }
840 RustType::JsValue(js_value) => {
841 let param_name = ¶m.name;
842 if js_value.is_option {
843 format!(
844 "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = null;\nif ({param_name}Temp_ !== null) {{{{ {param_name} = window[{param_name}Temp_] }}}};",
845 )
846 }
847 else {
848 format!(
849 "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = window[{param_name}Temp_];",
850 )
851 }
852 },
853 RustType::CallBack(rust_callback) => {
854 let name = ¶m.name;
855 let index = callback_name_to_index.get(name).unwrap();
856 let RustCallback { input, output } = rust_callback;
857 match (input, output) {
858 (None, None) => {
859 format!(
861 "const {} = async () => {{{{ dioxus.send([{}, null]); await dioxus.recv(); }}}};",
862 name, index
863 )
864 },
865 (None, Some(_)) => {
866 format!(
867 "const {} = async () => {{{{ dioxus.send([{}, null]); return await dioxus.recv(); }}}};",
868 name, index
869
870 )
871 },
872 (Some(_), None) => {
873 format!(
875 "const {} = async (value) => {{{{ dioxus.send([{}, value]); await dioxus.recv(); }}}};",
876 name, index
877 )
878 },
879 (Some(_), Some(_)) => {
880 format!(
881 "const {} = async (value) => {{{{ dioxus.send([{}, value]); return await dioxus.recv(); }}}};",
882 name, index
883 )
884 },
885 }
886 },
887 })
888 .collect::<Vec<_>>()
889 .join("\n");
890 let mut await_fn = String::new();
891 if func.is_async {
892 await_fn.push_str("await");
893 }
894 let call_function = match &func.rust_return_type {
895 RustType::Regular(_) => {
896 format!(
898 r#"
899___result___ = {await_fn} {js_func_name}({params_list});
900"#
901 )
902 }
903 RustType::CallBack(_) => panic!("Cannot be an output type, should have panicked earlier."),
904 RustType::JsValue(js_value) => {
905 let check = if js_value.is_option {
906 "if (___resultValue___ === null || ___resultValue___ === undefined) {{{{ return null; }}}}".to_owned()
908 } else {
909 format!(
910 "if (___resultValue___ === undefined) {{{{ console.error(\"`{js_func_name}` was undefined, but value is needed for JsValue\"); return null; }}}}"
911 )
912 };
913 format!(
914 r#"
915const ___resultValue___ = {await_fn} {js_func_name}({params_list});
916{check}
917___result___ = "js-value-{js_func_name}-" + crypto.randomUUID();
918window[___result___] = ___resultValue___;
919 "#
920 )
921 }
922 };
923 let end_statement = if callback_name_to_index.is_empty() {
924 "if (___result___ === undefined) {{ return null; }}; return ___result___;"
925 } else {
926 "if (___result___ === undefined) {{ dioxus.send([0, null]); }}; dioxus.send([0, ___result___]);"
927 };
928
929 let js_format = format!(
930 r#"
931const {{{{ {js_func_name} }}}} = await import("{{}}");
932{param_declaration_lines}
933let ___result___;
934try {{{{
935{call_function}
936}}}}
937catch (e) {{{{
938console.error("Executing function `{js_func_name}` threw an error:", e);
939___result___ = undefined;
940}}}}
941{end_statement}
942"#
943 );
944
945 let param_types: Vec<_> = func
947 .params
948 .iter()
949 .map(|param| {
950 let param_name = format_ident!("{}", param.name);
951 let type_tokens = param.rust_type.to_tokens();
952 if let RustType::CallBack(_) = param.rust_type {
953 quote! { mut #param_name: #type_tokens }
954 } else {
955 quote! { #param_name: #type_tokens }
956 }
957 })
958 .collect();
959
960 let parsed_type = func.rust_return_type.to_tokens();
961 let (return_type_tokens, generic_tokens) =
962 if func.rust_return_type.to_string() == DEFAULT_OUTPUT {
963 (
964 quote! { Result<T, dioxus_use_js::JsError> },
965 Some(quote! { <#parsed_type> }),
966 )
967 } else {
968 (
969 quote! { Result<#parsed_type, dioxus_use_js::JsError> },
970 None,
971 )
972 };
973
974 let doc_comment = if func.doc_comment.is_empty() {
976 quote! {}
977 } else {
978 let doc_lines: Vec<_> = func
979 .doc_comment
980 .iter()
981 .map(|line| quote! { #[doc = #line] })
982 .collect();
983 quote! { #(#doc_lines)* }
984 };
985
986 let func_name = func
987 .name_ident
988 .clone()
989 .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
991
992 let void_output_mapping = if func.rust_return_type.to_string() == "()" {
994 quote! {
995 .and_then(|e| {
996 if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
997 Ok(())
998 } else {
999 Err(dioxus_use_js::JsError::Eval(
1000 dioxus::document::EvalError::Serialization(
1001 <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1002 )
1003 ))
1004 }
1005 })
1006 }
1007 } else {
1008 quote! {}
1009 };
1010
1011 let has_no_callbacks = callback_name_to_index.is_empty();
1012 let end_statement = if has_no_callbacks {
1013 let return_value_mapping = if func.rust_return_type.to_string() == SERDE_VALUE_OUTPUT {
1014 quote! {
1015 .map_err(dioxus_use_js::JsError::Eval)
1016 }
1017 } else {
1018 quote! {
1019 .map_err(dioxus_use_js::JsError::Eval)
1020 .and_then(|v| dioxus_use_js::serde_json_from_value(v).map_err(|e| dioxus_use_js::JsError::Eval(dioxus::document::EvalError::Serialization(e))))
1021 }
1022 };
1023
1024 match &func.rust_return_type {
1025 RustType::Regular(_) => {
1026 quote! {
1027 eval
1028 .await
1029 #return_value_mapping
1030 #void_output_mapping
1031 }
1032 }
1033 RustType::CallBack(_) => {
1034 panic!("Cannot be an output type, should have panicked earlier.")
1035 }
1036 RustType::JsValue(js_value) => {
1037 if js_value.is_option {
1038 quote! {
1039 let id: Option<String> = eval
1040 .await
1041 #return_value_mapping?;
1042 #[allow(deprecated)]
1043 Ok(id.map(|e| dioxus_use_js::JsValue::internal_create(e)))
1044 }
1045 } else {
1046 quote! {
1047 let id: String = eval
1048 .await
1049 #return_value_mapping?;
1050 #[allow(deprecated)]
1051 Ok(dioxus_use_js::JsValue::internal_create(id))
1052 }
1053 }
1054 }
1055 }
1056 } else {
1057 let callback_arms: Vec<TokenStream2> = callback_name_to_index
1058 .iter()
1059 .map(|(name, index)| {
1060 let callback = callback_name_to_info.get(name).unwrap();
1061 let callback_call = if let Some(_) = callback.input {
1062 quote! {
1063 let value = dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1064 dioxus_use_js::JsError::Eval(
1065 dioxus::document::EvalError::Serialization(e),
1066 )
1067 })?;
1068 let value = match callback(value).await {
1069 Ok(value) => value,
1070 Err(error) => {
1071 return Err(dioxus_use_js::JsError::Callback(error));
1072 }
1073 };
1074 }
1075 } else {
1076 quote! {
1077 let value = match callback().await {
1078 Ok(value) => value,
1079 Err(error) => {
1080 return Err(dioxus_use_js::JsError::Callback(error));
1081 }
1082 };
1083 }
1084 };
1085
1086 let callback_send_back = if let Some(_) = callback.output {
1087 quote! {
1088 eval.send(value).map_err(dioxus_use_js::JsError::Eval)?;
1089 }
1090 } else {
1091 quote! {
1093 eval.send(dioxus_use_js::SerdeJsonValue::Null).map_err(dioxus_use_js::JsError::Eval)?;
1094 }
1095 };
1096 quote! {
1097 #index => {
1098 #callback_call
1099 #callback_send_back
1100 }
1101 }
1102 })
1103 .collect();
1104
1105 quote! {
1106 loop {
1107 let value = eval
1108 .recv::<dioxus_use_js::SerdeJsonValue>()
1109 .await
1110 .map_err(dioxus_use_js::JsError::Eval)?;
1111 match value{
1112 dioxus_use_js::SerdeJsonValue::Array(values) => {
1113 if values.len() != 2 {
1114 unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG)
1115 }
1116 let mut iter = values.into_iter();
1117 let action_ = match iter.next().unwrap() {
1118 dioxus_use_js::SerdeJsonValue::Number(action_) => action_,
1119 _ => unreachable!("{}", dioxus_use_js::__INDEX_VALIDATION_MSG),
1120 };
1121 let value = iter.next().unwrap();
1122 match action_.as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG) {
1123 0 => {
1124 return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1125 dioxus_use_js::JsError::Eval(
1126 dioxus::document::EvalError::Serialization(e),
1127 )
1128 })
1129 #void_output_mapping;
1130 }
1131 #(#callback_arms,)*
1132 _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1133 }
1134 }
1135 _ => unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG),
1136 }
1137 }
1138 }
1139 };
1140
1141 quote! {
1142 #doc_comment
1143 #[allow(non_snake_case)]
1144 pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1145 const MODULE: Asset = asset!(#asset_path);
1146 let js = format!(#js_format, MODULE);
1147 let mut eval = dioxus::document::eval(js.as_str());
1148 #(#send_calls)*
1149 #end_statement
1150 }
1151 }
1152}
1153
1154#[proc_macro]
1156pub fn use_js(input: TokenStream) -> TokenStream {
1157 let input = parse_macro_input!(input as UseJsInput);
1158
1159 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1160 Ok(dir) => dir,
1161 Err(_) => {
1162 return TokenStream::from(
1163 syn::Error::new(
1164 proc_macro2::Span::call_site(),
1165 "CARGO_MANIFEST_DIR environment variable not found",
1166 )
1167 .to_compile_error(),
1168 );
1169 }
1170 };
1171
1172 let UseJsInput {
1173 js_bundle_path,
1174 ts_source_path,
1175 import_spec,
1176 } = input;
1177
1178 let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1179
1180 let js_all_functions = match parse_script_file(&js_file_path, true) {
1181 Ok(funcs) => funcs,
1182 Err(e) => return TokenStream::from(e.to_compile_error()),
1183 };
1184
1185 let js_functions_to_generate =
1186 match get_functions_to_generate(js_all_functions, &import_spec, &js_file_path) {
1187 Ok(funcs) => funcs,
1188 Err(e) => return TokenStream::from(e.to_compile_error()),
1189 };
1190
1191 let functions_to_generate = if let Some(ts_file_path) = ts_source_path {
1192 let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1193 let ts_all_functions = match parse_script_file(&ts_file_path, false) {
1194 Ok(funcs) => funcs,
1195 Err(e) => return TokenStream::from(e.to_compile_error()),
1196 };
1197
1198 let ts_functions_to_generate =
1199 match get_functions_to_generate(ts_all_functions, &import_spec, &ts_file_path) {
1200 Ok(funcs) => funcs,
1201 Err(e) => {
1202 return TokenStream::from(e.to_compile_error());
1203 }
1204 };
1205
1206 for ts_func in ts_functions_to_generate.iter() {
1207 if let Some(js_func) = js_functions_to_generate
1208 .iter()
1209 .find(|f| f.name == ts_func.name)
1210 {
1211 if ts_func.params.len() != js_func.params.len() {
1212 return TokenStream::from(syn::Error::new(
1213 proc_macro2::Span::call_site(),
1214 format!(
1215 "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1216 ts_func.name
1217 ),
1218 )
1219 .to_compile_error());
1220 }
1221 } else {
1222 return TokenStream::from(syn::Error::new(
1223 proc_macro2::Span::call_site(),
1224 format!(
1225 "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1226 ts_func.name
1227 ),
1228 )
1229 .to_compile_error());
1230 }
1231 }
1232 ts_functions_to_generate
1233 } else {
1234 js_functions_to_generate
1235 };
1236
1237 let function_wrappers: Vec<TokenStream2> = functions_to_generate
1238 .iter()
1239 .map(|func| generate_function_wrapper(func, &js_bundle_path))
1240 .collect();
1241
1242 let expanded = quote! {
1243 #(#function_wrappers)*
1244 };
1245
1246 TokenStream::from(expanded)
1247}
1248
1249#[cfg(test)]
1252mod tests {
1253 use super::*;
1254
1255 #[test]
1256 fn test_primitives() {
1257 assert_eq!(
1258 ts_type_to_rust_type(Some("string"), false).to_string(),
1259 "String"
1260 );
1261 assert_eq!(
1262 ts_type_to_rust_type(Some("string"), true).to_string(),
1263 "&str"
1264 );
1265 assert_eq!(
1266 ts_type_to_rust_type(Some("number"), false).to_string(),
1267 "f64"
1268 );
1269 assert_eq!(
1270 ts_type_to_rust_type(Some("number"), true).to_string(),
1271 "f64"
1272 );
1273 assert_eq!(
1274 ts_type_to_rust_type(Some("boolean"), false).to_string(),
1275 "bool"
1276 );
1277 assert_eq!(
1278 ts_type_to_rust_type(Some("boolean"), true).to_string(),
1279 "bool"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_nullable_primitives() {
1285 assert_eq!(
1286 ts_type_to_rust_type(Some("string | null"), true).to_string(),
1287 "Option<&str>"
1288 );
1289 assert_eq!(
1290 ts_type_to_rust_type(Some("string | null"), false).to_string(),
1291 "Option<String>"
1292 );
1293 assert_eq!(
1294 ts_type_to_rust_type(Some("number | null"), true).to_string(),
1295 "Option<f64>"
1296 );
1297 assert_eq!(
1298 ts_type_to_rust_type(Some("number | null"), false).to_string(),
1299 "Option<f64>"
1300 );
1301 assert_eq!(
1302 ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1303 "Option<bool>"
1304 );
1305 assert_eq!(
1306 ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1307 "Option<bool>"
1308 );
1309 }
1310
1311 #[test]
1312 fn test_arrays() {
1313 assert_eq!(
1314 ts_type_to_rust_type(Some("string[]"), true).to_string(),
1315 "&[String]"
1316 );
1317 assert_eq!(
1318 ts_type_to_rust_type(Some("string[]"), false).to_string(),
1319 "Vec<String>"
1320 );
1321 assert_eq!(
1322 ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
1323 "&[f64]"
1324 );
1325 assert_eq!(
1326 ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
1327 "Vec<f64>"
1328 );
1329 }
1330
1331 #[test]
1332 fn test_nullable_array_elements() {
1333 assert_eq!(
1334 ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
1335 "&[Option<String>]"
1336 );
1337 assert_eq!(
1338 ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
1339 "Vec<Option<String>>"
1340 );
1341 assert_eq!(
1342 ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
1343 "&[Option<f64>]"
1344 );
1345 assert_eq!(
1346 ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
1347 "Vec<Option<f64>>"
1348 );
1349 }
1350
1351 #[test]
1352 fn test_nullable_array_itself() {
1353 assert_eq!(
1354 ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
1355 "Option<&[String]>"
1356 );
1357 assert_eq!(
1358 ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
1359 "Option<Vec<String>>"
1360 );
1361 assert_eq!(
1362 ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
1363 "Option<&[f64]>"
1364 );
1365 assert_eq!(
1366 ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
1367 "Option<Vec<f64>>"
1368 );
1369 }
1370
1371 #[test]
1372 fn test_nullable_array_and_elements() {
1373 assert_eq!(
1374 ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
1375 "Option<&[Option<String>]>"
1376 );
1377 assert_eq!(
1378 ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
1379 "Option<Vec<Option<String>>>"
1380 );
1381 }
1382
1383 #[test]
1384 fn test_fallback_for_union() {
1385 assert_eq!(
1386 ts_type_to_rust_type(Some("string | number"), true).to_string(),
1387 "impl dioxus_use_js::SerdeSerialize"
1388 );
1389 assert_eq!(
1390 ts_type_to_rust_type(Some("string | number"), false).to_string(),
1391 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1392 );
1393 assert_eq!(
1394 ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
1395 "impl dioxus_use_js::SerdeSerialize"
1396 );
1397 assert_eq!(
1398 ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
1399 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_unknown_types() {
1405 assert_eq!(
1406 ts_type_to_rust_type(Some("foo"), true).to_string(),
1407 "impl dioxus_use_js::SerdeSerialize"
1408 );
1409 assert_eq!(
1410 ts_type_to_rust_type(Some("foo"), false).to_string(),
1411 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1412 );
1413
1414 assert_eq!(
1415 ts_type_to_rust_type(Some("any"), true).to_string(),
1416 "impl dioxus_use_js::SerdeSerialize"
1417 );
1418 assert_eq!(
1419 ts_type_to_rust_type(Some("any"), false).to_string(),
1420 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1421 );
1422 assert_eq!(
1423 ts_type_to_rust_type(Some("object"), true).to_string(),
1424 "impl dioxus_use_js::SerdeSerialize"
1425 );
1426 assert_eq!(
1427 ts_type_to_rust_type(Some("object"), false).to_string(),
1428 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1429 );
1430 assert_eq!(
1431 ts_type_to_rust_type(Some("unknown"), true).to_string(),
1432 "impl dioxus_use_js::SerdeSerialize"
1433 );
1434 assert_eq!(
1435 ts_type_to_rust_type(Some("unknown"), false).to_string(),
1436 "T: dioxus_use_js::SerdeDeDeserializeOwned"
1437 );
1438
1439 assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
1440 assert_eq!(
1441 ts_type_to_rust_type(Some("undefined"), false).to_string(),
1442 "()"
1443 );
1444 assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
1445 }
1446
1447 #[test]
1448 fn test_extra_whitespace() {
1449 assert_eq!(
1450 ts_type_to_rust_type(Some(" string | null "), true).to_string(),
1451 "Option<&str>"
1452 );
1453 assert_eq!(
1454 ts_type_to_rust_type(Some(" string | null "), false).to_string(),
1455 "Option<String>"
1456 );
1457 assert_eq!(
1458 ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
1459 "&[String]"
1460 );
1461 assert_eq!(
1462 ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
1463 "Vec<String>"
1464 );
1465 }
1466
1467 #[test]
1468 fn test_map_types() {
1469 assert_eq!(
1470 ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
1471 "&std::collections::HashMap<String, f64>"
1472 );
1473 assert_eq!(
1474 ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
1475 "std::collections::HashMap<String, f64>"
1476 );
1477 assert_eq!(
1478 ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
1479 "&std::collections::HashMap<String, bool>"
1480 );
1481 assert_eq!(
1482 ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
1483 "std::collections::HashMap<String, bool>"
1484 );
1485 assert_eq!(
1486 ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
1487 "&std::collections::HashMap<f64, String>"
1488 );
1489 assert_eq!(
1490 ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
1491 "std::collections::HashMap<f64, String>"
1492 );
1493 }
1494
1495 #[test]
1496 fn test_set_types() {
1497 assert_eq!(
1498 ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
1499 "&std::collections::HashSet<String>"
1500 );
1501 assert_eq!(
1502 ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
1503 "std::collections::HashSet<String>"
1504 );
1505 assert_eq!(
1506 ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
1507 "&std::collections::HashSet<f64>"
1508 );
1509 assert_eq!(
1510 ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
1511 "std::collections::HashSet<f64>"
1512 );
1513 assert_eq!(
1514 ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
1515 "&std::collections::HashSet<bool>"
1516 );
1517 assert_eq!(
1518 ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
1519 "std::collections::HashSet<bool>"
1520 );
1521 }
1522
1523 #[test]
1524 fn test_rust_callback() {
1525 assert_eq!(
1526 ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
1527 "impl AsyncFnMut(f64) -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1528 );
1529 assert_eq!(
1530 ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
1531 "impl AsyncFnMut() -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1532 );
1533 assert_eq!(
1534 ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
1535 "impl AsyncFnMut() -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1536 );
1537 assert_eq!(
1538 ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
1539 "impl AsyncFnMut(f64) -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1540 );
1541 }
1542
1543 #[test]
1544 fn test_promise_types() {
1545 assert_eq!(
1546 ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
1547 "String"
1548 );
1549 assert_eq!(
1550 ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
1551 "f64"
1552 );
1553 assert_eq!(
1554 ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
1555 "bool"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_json_types() {
1561 assert_eq!(
1562 ts_type_to_rust_type(Some("Json"), true).to_string(),
1563 "&dioxus_use_js::SerdeJsonValue"
1564 );
1565 assert_eq!(
1566 ts_type_to_rust_type(Some("Json"), false).to_string(),
1567 "dioxus_use_js::SerdeJsonValue"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_js_value() {
1573 assert_eq!(
1574 ts_type_to_rust_type(Some("JsValue"), true).to_string(),
1575 "&dioxus_use_js::JsValue"
1576 );
1577 assert_eq!(
1578 ts_type_to_rust_type(Some("JsValue"), false).to_string(),
1579 "dioxus_use_js::JsValue"
1580 );
1581 assert_eq!(
1582 ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
1583 "&dioxus_use_js::JsValue"
1584 );
1585 assert_eq!(
1586 ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
1587 "dioxus_use_js::JsValue"
1588 );
1589
1590 assert_eq!(
1591 ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
1592 "dioxus_use_js::JsValue"
1593 );
1594
1595 assert_eq!(
1596 ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
1597 "Option<dioxus_use_js::JsValue>"
1598 );
1599 assert_eq!(
1600 ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
1601 "Option<&dioxus_use_js::JsValue>"
1602 );
1603 assert_eq!(
1604 ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
1605 "Option<dioxus_use_js::JsValue>"
1606 );
1607 }
1608}