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