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