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, comments::SingleThreadedComments};
14use swc_common::{SourceMapper, Spanned};
15use swc_ecma_ast::{
16 ClassDecl, ClassMember, Decl, ExportDecl, ExportSpecifier, FnDecl, NamedExport, Pat, PropName,
17 TsType, TsTypeAnn, VarDeclarator,
18};
19use swc_ecma_parser::EsSyntax;
20use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
21use swc_ecma_visit::{Visit, VisitWith};
22use syn::TypeParam;
23use syn::{
24 Ident, LitStr, Result, Token,
25 parse::{Parse, ParseStream},
26 parse_macro_input,
27};
28
29const JSVALUE_START: &str = "JsValue";
31const JSVALUE: &str = "dioxus_use_js::JsValue";
32const DEFAULT_GENRIC_INPUT: &str = "impl dioxus_use_js::SerdeSerialize";
33const DEFAULT_GENERIC_OUTPUT: &str = "DeserializeOwned";
34const DEFAULT_OUTPUT_GENERIC_DECLARTION: &str =
35 "DeserializeOwned: dioxus_use_js::SerdeDeDeserializeOwned";
36const SERDE_VALUE: &str = "dioxus_use_js::SerdeJsonValue";
37const JSON: &str = "Json";
38const RUST_CALLBACK_JS_START: &str = "RustCallback";
40const UNIT: &str = "()";
41const DROP_TYPE: &str = "Drop";
42const DROP_NAME: &str = "drop";
43
44#[derive(Debug, Clone)]
45enum ImportSpec {
46 All,
48 Named(Vec<Ident>),
50 Single(Ident),
52}
53
54struct UseJsInput {
55 js_bundle_path: LitStr,
56 ts_source_path: Option<LitStr>,
57 import_spec: ImportSpec,
58}
59
60impl Parse for UseJsInput {
61 fn parse(input: ParseStream) -> Result<Self> {
62 let first_str: LitStr = input.parse()?;
63
64 let (ts_source_path, js_bundle_path) = if input.peek(Token![,]) {
66 input.parse::<Token![,]>()?;
67 let second_str: LitStr = input.parse()?;
68 (Some(first_str), second_str)
69 } else {
70 (None, first_str)
71 };
72
73 let import_spec = if input.peek(Token![::]) {
75 input.parse::<Token![::]>()?;
76
77 if input.peek(Token![*]) {
78 input.parse::<Token![*]>()?;
79 ImportSpec::All
80 } else if input.peek(Ident) {
81 let ident: Ident = input.parse()?;
82 ImportSpec::Single(ident)
83 } else if input.peek(syn::token::Brace) {
84 let content;
85 syn::braced!(content in input);
86 let idents: syn::punctuated::Punctuated<Ident, Token![,]> =
87 content.parse_terminated(Ident::parse, Token![,])?;
88 ImportSpec::Named(idents.into_iter().collect())
89 } else {
90 return Err(input.error("Expected `*`, an identifier, or a brace group after `::`"));
91 }
92 } else {
93 return Err(input
94 .error("Expected `::` followed by an import spec (even for wildcard with `*`)"));
95 };
96
97 Ok(UseJsInput {
98 js_bundle_path,
99 ts_source_path,
100 import_spec,
101 })
102 }
103}
104
105#[derive(Debug, Clone)]
106struct ParamInfo {
107 name: String,
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 ident: Option<Ident>,
126 params: Vec<ParamInfo>,
128 js_return_type: Option<String>,
130 rust_return_type: RustType,
131 is_exported: bool,
132 is_async: bool,
133 doc_comment: Vec<String>,
135}
136
137#[derive(Debug, Clone)]
138struct MethodInfo {
139 name: String,
140 params: Vec<ParamInfo>,
142 js_return_type: Option<String>,
144 rust_return_type: RustType,
145 is_async: bool,
146 is_static: bool,
147 doc_comment: Vec<String>,
149}
150
151#[derive(Debug, Clone)]
152#[allow(dead_code)]
153struct ClassInfo {
154 name: String,
155 ident: Option<Ident>,
157 methods: Vec<MethodInfo>,
159 is_exported: bool,
160 doc_comment: Vec<String>,
162}
163
164struct JsVisitor {
165 functions: Vec<FunctionInfo>,
166 classes: Vec<ClassInfo>,
167 comments: SingleThreadedComments,
168 source_map: SourceMap,
169}
170
171impl JsVisitor {
172 fn new(comments: SingleThreadedComments, source_map: SourceMap) -> Self {
173 Self {
174 functions: Vec::new(),
175 classes: Vec::new(),
176 comments,
177 source_map,
178 }
179 }
180
181 fn extract_doc_comment(&self, span: &swc_common::Span) -> Vec<String> {
182 let leading_comment = self.comments.get_leading(span.lo());
184
185 if let Some(comments) = leading_comment {
186 let mut doc_lines = Vec::new();
187
188 for comment in comments.iter() {
189 let comment_text = &comment.text;
190 match comment.kind {
191 CommentKind::Line => {
193 if let Some(content) = comment_text.strip_prefix("/") {
194 let cleaned = content.trim_start();
195 doc_lines.push(cleaned.to_string());
196 }
197 }
198 CommentKind::Block => {
200 for line in comment_text.lines() {
201 if let Some(cleaned) = line.trim_start().strip_prefix("*") {
202 doc_lines.push(cleaned.to_string());
203 }
204 }
205 }
206 };
207 }
208
209 doc_lines
210 } else {
211 Vec::new()
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
217enum RustType {
218 Regular(String),
219 Callback(RustCallback),
220 JsValue(JsValue),
221}
222
223impl ToString for RustType {
224 fn to_string(&self) -> String {
225 match self {
226 RustType::Regular(ty) => ty.clone(),
227 RustType::Callback(callback) => callback.to_string(),
228 RustType::JsValue(js_value) => js_value.to_string(),
229 }
230 }
231}
232
233impl RustType {
234 fn to_tokens(&self) -> TokenStream2 {
235 self.to_string()
236 .parse::<TokenStream2>()
237 .expect("Calculated Rust type should always be valid")
238 }
239}
240
241#[derive(Debug, Clone)]
242struct RustCallback {
243 input: Option<String>,
244 output: Option<String>,
245}
246
247impl ToString for RustCallback {
248 fn to_string(&self) -> String {
249 let input = self.input.as_deref();
250 let output = self.output.as_deref().unwrap_or(UNIT);
251 format!(
252 "dioxus::core::Callback<{}, impl Future<Output = Result<{}, dioxus_use_js::SerdeJsonValue>> + 'static>",
253 input.unwrap_or("()"),
254 output
255 )
256 }
257}
258
259#[derive(Debug, Clone)]
260struct JsValue {
261 is_option: bool,
262 is_input: bool,
263}
264
265impl ToString for JsValue {
266 fn to_string(&self) -> String {
267 if self.is_option {
268 format!(
269 "Option<{}>",
270 if self.is_input {
271 format!("&{}", JSVALUE)
272 } else {
273 JSVALUE.to_owned()
274 }
275 )
276 } else {
277 if self.is_input {
278 format!("&{}", JSVALUE)
279 } else {
280 JSVALUE.to_owned()
281 }
282 }
283 }
284}
285
286fn strip_parenthesis(mut ts_type: &str) -> &str {
287 while ts_type.starts_with("(") && ts_type.ends_with(")") {
288 ts_type = &ts_type[1..ts_type.len() - 1].trim();
289 }
290 return ts_type;
291}
292
293fn split_into_args(ts_type: &str) -> Vec<&str> {
295 let mut depth_angle: u16 = 0;
296 let mut depth_square: u16 = 0;
297 let mut depth_paren: u16 = 0;
298 let mut splits = Vec::new();
299 let mut last: usize = 0;
300 for (i, c) in ts_type.char_indices() {
301 match c {
302 '<' => depth_angle += 1,
303 '>' => depth_angle = depth_angle.saturating_sub(1),
304 '[' => depth_square += 1,
305 ']' => depth_square = depth_square.saturating_sub(1),
306 '(' => depth_paren += 1,
307 ')' => depth_paren = depth_paren.saturating_sub(1),
308 ',' if depth_angle == 0 && depth_square == 0 && depth_paren == 0 => {
309 splits.push(ts_type[last..i].trim());
310 last = i + 1;
311 }
312 _ => {}
313 }
314 }
315 let len = ts_type.len();
316 if last != len {
317 let maybe_arg = ts_type[last..len].trim();
318 if !maybe_arg.is_empty() {
319 splits.push(maybe_arg);
320 }
321 }
322 splits
323}
324
325fn ts_type_to_rust_type(ts_type: Option<&str>, is_input: bool) -> RustType {
326 let Some(mut ts_type) = ts_type else {
327 return RustType::Regular(
328 (if is_input {
329 DEFAULT_GENRIC_INPUT
330 } else {
331 DEFAULT_GENERIC_OUTPUT
332 })
333 .to_owned(),
334 );
335 };
336 ts_type = strip_parenthesis(&mut ts_type);
337 if ts_type.starts_with("Promise<") && ts_type.ends_with(">") {
338 assert!(!is_input, "Promise cannot be used as input type");
339 ts_type = &ts_type[8..ts_type.len() - 1];
340 }
341 ts_type = strip_parenthesis(&mut ts_type);
342 if ts_type.contains(JSVALUE_START) {
343 let parts = split_top_level_union(ts_type);
344 let len = parts.len();
345 if len == 1 && parts[0].starts_with(JSVALUE_START) {
346 return RustType::JsValue(JsValue {
347 is_option: false,
348 is_input,
349 });
350 }
351
352 if len == 2 && parts.contains(&"null") {
353 return RustType::JsValue(JsValue {
354 is_option: true,
355 is_input,
356 });
357 } else {
358 panic!("Invalid use of `{}` for `{}`", JSVALUE_START, ts_type);
359 }
360 }
361 if ts_type.contains(RUST_CALLBACK_JS_START) {
362 if !ts_type.starts_with(RUST_CALLBACK_JS_START) {
363 panic!("Nested RustCallback is not valid: {}", ts_type);
364 }
365 assert!(is_input, "Cannot return a RustCallback: {}", ts_type);
366 let ts_type = &ts_type[RUST_CALLBACK_JS_START.len()..];
367 if !(ts_type.starts_with("<") && ts_type.ends_with(">")) {
368 panic!("Invalid RustCallback type: {}", ts_type);
369 }
370 let inner = &ts_type[1..ts_type.len() - 1];
371 let parts = split_into_args(inner);
372 let len = parts.len();
373 if len != 2 {
374 panic!(
375 "A RustCallback type expects two parameters, got: {:?}",
376 parts
377 );
378 }
379 let ts_input = parts[0];
380 let rs_input = if ts_input == "void" {
381 None
382 } else {
383 let rs_input = ts_type_to_rust_type_helper(ts_input, false);
384 if rs_input.is_none() || rs_input.as_ref().is_some_and(|e| e == UNIT) {
385 panic!("Type `{ts_input}` is not a valid input for `{RUST_CALLBACK_JS_START}`");
386 }
387 rs_input
388 };
389 let ts_output = parts[1];
390 let rs_output = if ts_output == "void" {
391 None
392 } else {
393 let rs_output = ts_type_to_rust_type_helper(ts_output, false);
394 if rs_output.is_none() || rs_output.as_ref().is_some_and(|e| e == UNIT) {
395 panic!("Type `{ts_output}` is not a valid output for `{RUST_CALLBACK_JS_START}`");
396 }
397 rs_output
398 };
399 return RustType::Callback(RustCallback {
400 input: rs_input,
401 output: rs_output,
402 });
403 }
404 RustType::Regular(match ts_type_to_rust_type_helper(ts_type, is_input) {
405 Some(value) => {
406 if value.contains(UNIT) && (is_input || &value != UNIT) {
407 panic!("`{}` is not valid in this position", ts_type);
410 }
411 value
412 }
413 None => (if is_input {
414 DEFAULT_GENRIC_INPUT
415 } else {
416 DEFAULT_GENERIC_OUTPUT
417 })
418 .to_owned(),
419 })
420}
421
422fn ts_type_to_rust_type_helper(mut ts_type: &str, can_be_ref: bool) -> Option<String> {
424 ts_type = ts_type.trim();
425 ts_type = strip_parenthesis(&mut ts_type);
426
427 let parts = split_top_level_union(ts_type);
428 if parts.len() > 1 {
429 if parts.len() == 2 && parts.contains(&"null") {
431 let inner = parts.iter().find(|p| **p != "null")?;
432 let inner_rust = ts_type_to_rust_type_helper(inner, can_be_ref)?;
433 return Some(format!("Option<{}>", inner_rust));
434 }
435 return None;
437 }
438
439 ts_type = parts[0];
440
441 if ts_type.ends_with("[]") {
442 let inner = ts_type.strip_suffix("[]").unwrap();
443 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
444 return Some(if can_be_ref {
445 format!("&[{}]", inner_rust)
446 } else {
447 format!("Vec<{}>", inner_rust)
448 });
449 }
450
451 if ts_type.starts_with("Array<") && ts_type.ends_with(">") {
452 let inner = &ts_type[6..ts_type.len() - 1];
453 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
454 return Some(if can_be_ref {
455 format!("&[{}]", inner_rust)
456 } else {
457 format!("Vec<{}>", inner_rust)
458 });
459 }
460
461 if ts_type.starts_with("Set<") && ts_type.ends_with(">") {
462 let inner = &ts_type[4..ts_type.len() - 1];
463 let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
464 if can_be_ref {
465 return Some(format!("&std::collections::HashSet<{}>", inner_rust));
466 } else {
467 return Some(format!("std::collections::HashSet<{}>", inner_rust));
468 }
469 }
470
471 if ts_type.starts_with("Map<") && ts_type.ends_with(">") {
472 let inner = &ts_type[4..ts_type.len() - 1];
473 let mut depth = 0;
474 let mut split_index = None;
475 for (i, c) in inner.char_indices() {
476 match c {
477 '<' => depth += 1,
478 '>' => depth -= 1,
479 ',' if depth == 0 => {
480 split_index = Some(i);
481 break;
482 }
483 _ => {}
484 }
485 }
486
487 if let Some(i) = split_index {
488 let (key, value) = inner.split_at(i);
489 let value = &value[1..]; let key_rust = ts_type_to_rust_type_helper(key.trim(), false)?;
491 let value_rust = ts_type_to_rust_type_helper(value.trim(), false)?;
492 if can_be_ref {
493 return Some(format!(
494 "&std::collections::HashMap<{}, {}>",
495 key_rust, value_rust
496 ));
497 } else {
498 return Some(format!(
499 "std::collections::HashMap<{}, {}>",
500 key_rust, value_rust
501 ));
502 }
503 } else {
504 return None;
505 }
506 }
507
508 let rust_type = match ts_type {
510 "string" => {
511 if can_be_ref {
512 Some("&str".to_owned())
513 } else {
514 Some("String".to_owned())
515 }
516 }
517 "number" => Some("f64".to_owned()),
518 "boolean" => Some("bool".to_owned()),
519 "void" | "undefined" | "never" | "null" => Some(UNIT.to_owned()),
520 JSON => {
521 if can_be_ref {
522 Some(format!("&{SERDE_VALUE}"))
523 } else {
524 Some(SERDE_VALUE.to_owned())
525 }
526 }
527 "Promise" => {
528 panic!("`{}` - nested promises are not valid", ts_type)
529 }
530 _ => None,
532 };
533
534 rust_type
535}
536
537fn split_top_level_union(s: &str) -> Vec<&str> {
539 let mut parts = vec![];
540 let mut last = 0;
541 let mut depth_angle = 0;
542 let mut depth_paren = 0;
543
544 for (i, c) in s.char_indices() {
545 match c {
546 '<' => depth_angle += 1,
547 '>' => {
548 if depth_angle > 0 {
549 depth_angle -= 1
550 }
551 }
552 '(' => depth_paren += 1,
553 ')' => {
554 if depth_paren > 0 {
555 depth_paren -= 1
556 }
557 }
558 '|' if depth_angle == 0 && depth_paren == 0 => {
559 parts.push(s[last..i].trim());
560 last = i + 1;
561 }
562 _ => {}
563 }
564 }
565
566 if last < s.len() {
567 parts.push(s[last..].trim());
568 }
569
570 parts
571}
572
573fn type_to_string(ty: &Box<TsType>, source_map: &SourceMap) -> String {
574 let span = ty.span();
575 source_map
576 .span_to_snippet(span)
577 .expect("Could not get snippet from span for type")
578}
579
580fn function_pat_to_param_info<'a, I>(pats: I, source_map: &SourceMap) -> Vec<ParamInfo>
581where
582 I: Iterator<Item = &'a Pat>,
583{
584 pats.enumerate()
585 .map(|(i, pat)| to_param_info_helper(i, pat, source_map))
586 .collect()
587}
588
589fn to_param_info_helper(i: usize, pat: &Pat, source_map: &SourceMap) -> ParamInfo {
590 let name = if let Some(ident) = pat.as_ident() {
591 ident.id.sym.to_string()
592 } else {
593 format!("arg{}", i)
594 };
595
596 let js_type = pat
597 .as_ident()
598 .and_then(|ident| ident.type_ann.as_ref())
599 .map(|type_ann| {
600 let ty = &type_ann.type_ann;
601 type_to_string(ty, source_map)
602 });
603 let rust_type = ts_type_to_rust_type(js_type.as_deref(), true);
604
605 ParamInfo {
606 name,
607 js_type,
608 rust_type,
609 }
610}
611
612fn function_info_helper<'a, I>(
613 visitor: &JsVisitor,
614 name: String,
615 span: &swc_common::Span,
616 params: I,
617 return_type: Option<&Box<TsTypeAnn>>,
618 is_async: bool,
619 is_exported: bool,
620) -> FunctionInfo
621where
622 I: Iterator<Item = &'a Pat>,
623{
624 let doc_comment = visitor.extract_doc_comment(span);
625
626 let params = function_pat_to_param_info(params, &visitor.source_map);
627
628 let js_return_type = return_type.as_ref().map(|type_ann| {
629 let ty = &type_ann.type_ann;
630 type_to_string(ty, &visitor.source_map)
631 });
632 if !is_async
633 && let Some(ref js_return_type) = js_return_type
634 && js_return_type.starts_with("Promise")
635 {
636 panic!(
637 "Promise return type is only supported for async functions, use `async fn` instead. For `{js_return_type}`"
638 );
639 }
640
641 let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
642
643 FunctionInfo {
644 name,
645 ident: None,
646 params,
647 js_return_type,
648 rust_return_type,
649 is_exported,
650 is_async,
651 doc_comment,
652 }
653}
654
655impl Visit for JsVisitor {
656 fn visit_fn_decl(&mut self, node: &FnDecl) {
658 let name = node.ident.sym.to_string();
659 self.functions.push(function_info_helper(
660 self,
661 name,
662 &node.span(),
663 node.function.params.iter().map(|e| &e.pat),
664 node.function.return_type.as_ref(),
665 node.function.is_async,
666 false,
667 ));
668 node.visit_children_with(self);
669 }
670
671 fn visit_var_declarator(&mut self, node: &VarDeclarator) {
673 if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
674 if let Some(init) = &node.init {
675 let span = node.span();
676 let name = ident.id.sym.to_string();
677 match &**init {
678 swc_ecma_ast::Expr::Fn(fn_expr) => {
679 self.functions.push(function_info_helper(
680 &self,
681 name,
682 &span,
683 fn_expr.function.params.iter().map(|e| &e.pat),
684 fn_expr.function.return_type.as_ref(),
685 fn_expr.function.is_async,
686 false,
687 ));
688 }
689 swc_ecma_ast::Expr::Arrow(arrow_fn) => {
690 self.functions.push(function_info_helper(
691 &self,
692 name,
693 &span,
694 arrow_fn.params.iter(),
695 arrow_fn.return_type.as_ref(),
696 arrow_fn.is_async,
697 false,
698 ));
699 }
700 _ => {}
701 }
702 }
703 }
704 node.visit_children_with(self);
705 }
706
707 fn visit_export_decl(&mut self, node: &ExportDecl) {
709 match &node.decl {
710 Decl::Fn(fn_decl) => {
711 let span = node.span();
712 let name = fn_decl.ident.sym.to_string();
713 self.functions.push(function_info_helper(
714 &self,
715 name,
716 &span,
717 fn_decl.function.params.iter().map(|e| &e.pat),
718 fn_decl.function.return_type.as_ref(),
719 fn_decl.function.is_async,
720 true,
721 ));
722 }
723 Decl::Class(class_decl) => {
724 let name = class_decl.ident.sym.to_string();
725 let span = class_decl.class.span();
726 let doc_comment = self.extract_doc_comment(&span);
727 let mut methods = Vec::new();
728
729 for member in &class_decl.class.body {
730 match member {
731 ClassMember::Method(method) => {
732 let method_name = match &method.key {
733 PropName::Ident(ident) => ident.sym.to_string(),
734 PropName::Str(str_lit) => str_lit.value.to_string(),
735 _ => continue,
736 };
737
738 let method_span = method.span();
739 let method_doc = self.extract_doc_comment(&method_span);
740
741 let params = function_pat_to_param_info(
742 method.function.params.iter().map(|p| &p.pat),
743 &self.source_map,
744 );
745
746 let js_return_type =
747 method.function.return_type.as_ref().map(|type_ann| {
748 let ty = &type_ann.type_ann;
749 type_to_string(ty, &self.source_map)
750 });
751
752 let is_async = method.function.is_async;
753 if !is_async
754 && js_return_type
755 .as_ref()
756 .is_some_and(|js_return_type: &String| {
757 js_return_type.starts_with("Promise")
758 })
759 {
760 panic!(
761 "Method `{}` in exported class `{}` returns a Promise but is not marked as async",
762 method_name, name
763 );
764 }
765
766 let rust_return_type =
767 ts_type_to_rust_type(js_return_type.as_deref(), false);
768
769 methods.push(MethodInfo {
770 name: method_name,
771 params,
772 js_return_type,
773 rust_return_type,
774 is_async,
775 is_static: method.is_static,
776 doc_comment: method_doc,
777 });
778 }
779 _ => {}
780 }
781 }
782
783 self.classes.push(ClassInfo {
784 name,
785 ident: None,
786 methods,
787 is_exported: true,
788 doc_comment,
789 });
790 }
791 _ => {}
792 }
793 node.visit_children_with(self);
794 }
795
796 fn visit_named_export(&mut self, node: &NamedExport) {
798 for spec in &node.specifiers {
799 if let ExportSpecifier::Named(named) = spec {
800 let original_name = named.orig.atom().to_string();
801 let out_name = named
802 .exported
803 .as_ref()
804 .map(|e| e.atom().to_string())
805 .unwrap_or_else(|| original_name.clone());
806
807 if let Some(func) = self.functions.iter_mut().find(|f| f.name == original_name) {
809 let mut func = func.clone();
810 func.name = out_name.clone();
811 func.is_exported = true;
812 self.functions.push(func);
813 }
814 if let Some(class) = self.classes.iter_mut().find(|c| c.name == original_name) {
815 let mut class = class.clone();
816 class.name = out_name.clone();
817 class.is_exported = true;
818 self.classes.push(class);
819 }
820 }
821 }
822 node.visit_children_with(self);
823 }
824
825 fn visit_class_decl(&mut self, node: &ClassDecl) {
827 let name = node.ident.sym.to_string();
828 let span = node.span();
829 let doc_comment = self.extract_doc_comment(&span);
830 let mut methods = Vec::new();
831
832 for member in &node.class.body {
833 match member {
834 ClassMember::Method(method) => {
835 let method_name = match &method.key {
836 PropName::Ident(ident) => ident.sym.to_string(),
837 PropName::Str(str_lit) => str_lit.value.to_string(),
838 _ => continue,
839 };
840
841 let method_span = method.span();
842 let method_doc = self.extract_doc_comment(&method_span);
843
844 let params = function_pat_to_param_info(
845 method.function.params.iter().map(|p| &p.pat),
846 &self.source_map,
847 );
848
849 let js_return_type = method.function.return_type.as_ref().map(|type_ann| {
850 let ty = &type_ann.type_ann;
851 type_to_string(ty, &self.source_map)
852 });
853
854 let is_async = method.function.is_async;
855 if !is_async
856 && js_return_type
857 .as_ref()
858 .is_some_and(|js_return_type: &String| {
859 js_return_type.starts_with("Promise")
860 })
861 {
862 panic!(
863 "Function `{}` in class `{}` returns a Promise but is not marked as async",
864 method_name, name
865 );
866 }
867
868 let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
869
870 methods.push(MethodInfo {
871 name: method_name,
872 params,
873 js_return_type,
874 rust_return_type,
875 is_async,
876 is_static: method.is_static,
877 doc_comment: method_doc,
878 });
879 }
880 _ => {}
881 }
882 }
883
884 self.classes.push(ClassInfo {
885 name,
886 ident: None,
887 methods,
888 is_exported: false,
889 doc_comment,
890 });
891
892 node.visit_children_with(self);
893 }
894}
895
896fn parse_script_file(file_path: &Path, is_js: bool) -> Result<(Vec<FunctionInfo>, Vec<ClassInfo>)> {
897 let js_content = fs::read_to_string(file_path).map_err(|e| {
898 syn::Error::new(
899 proc_macro2::Span::call_site(),
900 format!("Could not read file '{}': {}", file_path.display(), e),
901 )
902 })?;
903
904 let source_map = SourceMap::default();
905 let fm = source_map.new_source_file(
906 swc_common::FileName::Custom(file_path.display().to_string()).into(),
907 js_content.clone(),
908 );
909 let comments = SingleThreadedComments::default();
910
911 let syntax = if is_js {
913 Syntax::Es(EsSyntax {
914 jsx: false,
915 fn_bind: false,
916 decorators: false,
917 decorators_before_export: false,
918 export_default_from: false,
919 import_attributes: false,
920 allow_super_outside_method: false,
921 allow_return_outside_function: false,
922 auto_accessors: false,
923 explicit_resource_management: false,
924 })
925 } else {
926 Syntax::Typescript(swc_ecma_parser::TsSyntax {
927 tsx: false,
928 decorators: false,
929 dts: false,
930 no_early_errors: false,
931 disallow_ambiguous_jsx_like: true,
932 })
933 };
934
935 let lexer = Lexer::new(
936 syntax,
937 Default::default(),
938 StringInput::from(&*fm),
939 Some(&comments),
940 );
941
942 let mut parser = Parser::new_from(lexer);
943
944 let module = parser.parse_module().map_err(|e| {
945 syn::Error::new(
946 proc_macro2::Span::call_site(),
947 format!(
948 "Failed to parse script file '{}': {:?}",
949 file_path.display(),
950 e
951 ),
952 )
953 })?;
954
955 let mut visitor = JsVisitor::new(comments, source_map);
956 module.visit_with(&mut visitor);
957
958 Ok((visitor.functions, visitor.classes))
959}
960
961fn get_types_to_generate(
962 classes: Vec<ClassInfo>,
963 functions: Vec<FunctionInfo>,
964 import_spec: &ImportSpec,
965 file: &Path,
966) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
967 fn named_helper(
968 names: &Vec<Ident>,
969 mut all_class_infos: Vec<ClassInfo>,
970 mut all_function_infos: Vec<FunctionInfo>,
971 file: &Path,
972 ) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
973 let mut resolved_function_infos = Vec::new();
974 let mut resolved_class_infos = Vec::new();
975 for name in names {
976 let name_str = name.to_string();
977
978 if let Some(pos) = all_function_infos
979 .iter()
980 .position(|f: &FunctionInfo| f.name == name_str && f.is_exported)
981 {
982 let mut function_info = all_function_infos.remove(pos);
983 function_info.ident.replace(name.clone());
984 resolved_function_infos.push(function_info);
985 continue;
986 }
987 if let Some(pos) = all_class_infos
988 .iter()
989 .position(|c: &ClassInfo| c.name == name_str && c.is_exported)
990 {
991 let mut class_info = all_class_infos.remove(pos);
992 class_info.ident.replace(name.clone());
993 resolved_class_infos.push(class_info);
994 continue;
995 }
996 if all_function_infos.iter().any(|f| f.name == name_str) {
997 return Err(syn::Error::new(
998 proc_macro2::Span::call_site(),
999 format!(
1000 "Function '{}' not exported in file '{}'",
1001 name,
1002 file.display()
1003 ),
1004 ));
1005 }
1006 if all_class_infos.iter().any(|c| c.name == name_str) {
1007 return Err(syn::Error::new(
1008 proc_macro2::Span::call_site(),
1009 format!("Class '{}' not exported in file '{}'", name, file.display()),
1010 ));
1011 }
1012 return Err(syn::Error::new(
1013 proc_macro2::Span::call_site(),
1014 format!(
1015 "Function or Class '{}' not found in file '{}'",
1016 name,
1017 file.display()
1018 ),
1019 ));
1020 }
1021 Ok((resolved_class_infos, resolved_function_infos))
1022 }
1023 match import_spec {
1024 ImportSpec::All => Ok((
1025 classes.into_iter().filter(|e| e.is_exported).collect(),
1026 functions.into_iter().filter(|e| e.is_exported).collect(),
1027 )),
1028 ImportSpec::Single(name) => named_helper(&vec![name.clone()], classes, functions, file),
1029 ImportSpec::Named(names) => named_helper(names, classes, functions, file),
1030 }
1031}
1032
1033fn generate_class_wrapper(
1034 class_info: &ClassInfo,
1035 asset_path: &LitStr,
1036 function_id_hasher: &blake3::Hasher,
1037) -> TokenStream2 {
1038 let class_ident = class_info
1039 .ident
1040 .clone()
1041 .unwrap_or_else(|| Ident::new(class_info.name.as_str(), proc_macro2::Span::call_site()));
1042
1043 let doc_comment = if class_info.doc_comment.is_empty() {
1044 quote! {}
1045 } else {
1046 let doc_lines: Vec<_> = class_info
1047 .doc_comment
1048 .iter()
1049 .map(|line| quote! { #[doc = #line] })
1050 .collect();
1051 quote! { #(#doc_lines)* }
1052 };
1053
1054 let mut parts: Vec<TokenStream2> = Vec::new();
1055 for method in &class_info.methods {
1056 let func_info = FunctionInfo {
1057 name: method.name.clone(),
1058 ident: None,
1059 params: method.params.clone(),
1060 js_return_type: method.js_return_type.clone(),
1061 rust_return_type: method.rust_return_type.clone(),
1062 is_exported: true,
1063 is_async: method.is_async,
1064 doc_comment: method.doc_comment.clone(),
1065 };
1066
1067 let inner_function = generate_invocation(
1068 Some(FunctionClassContext {
1069 class_name: class_info.name.clone(),
1070 ident: class_ident.clone(),
1071 is_static: method.is_static,
1072 }),
1073 &func_info,
1074 asset_path,
1075 function_id_hasher,
1076 );
1077
1078 let method_name = format_ident!("{}", method.name);
1079 let method_params: Vec<_> = method
1080 .params
1081 .iter()
1082 .filter_map(|param| {
1083 if param.is_drop() {
1084 return None;
1085 }
1086 let param_name = format_ident!("{}", param.name);
1087 let type_tokens = param.rust_type.to_tokens();
1088 Some(quote! { #param_name: #type_tokens })
1089 })
1090 .collect();
1091
1092 let param_names: Vec<_> = method
1093 .params
1094 .iter()
1095 .map(|p| format_ident!("{}", p.name))
1096 .collect();
1097
1098 let method_doc = if method.doc_comment.is_empty() {
1099 quote! {}
1100 } else {
1101 let doc_lines: Vec<_> = method
1102 .doc_comment
1103 .iter()
1104 .map(|line| quote! { #[doc = #line] })
1105 .collect();
1106 quote! { #(#doc_lines)* }
1107 };
1108
1109 fn returns_self_type(func_info: &FunctionInfo, class_info: &ClassInfo) -> bool {
1110 let Some(js_return_type) = &func_info.js_return_type else {
1111 return false;
1112 };
1113 if !matches!(func_info.rust_return_type, RustType::JsValue(_)) {
1114 return false;
1115 }
1116 if js_return_type.starts_with("JsValue<") && js_return_type.ends_with('>') {
1117 let inner = js_return_type[8..js_return_type.len() - 1].trim();
1118 if inner == class_info.name {
1119 return true;
1120 }
1121 }
1122 return false;
1123 }
1124
1125 let (invocation, return_type, generic) = if returns_self_type(&func_info, &class_info) {
1126 let invocation = if method.is_static {
1128 quote! {
1129 Ok(#class_ident::new(#method_name(#(#param_names),*).await?))
1130 }
1131 } else {
1132 quote! {
1133 Ok(#class_ident::new(#method_name(&self.0, #(#param_names),*).await?))
1134 }
1135 };
1136 let (_, generic_tokens) = return_type_tokens(
1137 &method.rust_return_type,
1138 class_info.ident.as_ref().map(|e| e.span()),
1139 );
1140 let return_type_tokens = quote! { Result<#class_ident, dioxus_use_js::JsError> };
1141 (invocation, return_type_tokens, generic_tokens)
1142 } else {
1143 let invocation = if method.is_static {
1144 quote! {
1145 #method_name(#(#param_names),*).await
1146 }
1147 } else {
1148 quote! {
1149 #method_name(&self.0, #(#param_names),*).await
1150 }
1151 };
1152 let (return_type_tokens, generic_tokens) = return_type_tokens(
1153 &method.rust_return_type,
1154 class_info.ident.as_ref().map(|e| e.span()),
1155 );
1156 (invocation, return_type_tokens, generic_tokens)
1157 };
1158
1159 let part = if method.is_static {
1160 quote! {
1161 #method_doc
1162 #[allow(non_snake_case)]
1163 pub async fn #method_name #generic(#(#method_params),*) -> #return_type {
1164 #[inline]
1165 #inner_function
1166 #invocation
1167 }
1168 }
1169 } else {
1170 quote! {
1171 #method_doc
1172 #[allow(non_snake_case)]
1173 pub async fn #method_name #generic(&self, #(#method_params),*) -> #return_type {
1174 #[inline]
1175 #inner_function
1176 #invocation
1177 }
1178 }
1179 };
1180
1181 parts.push(part);
1182 }
1183
1184 quote! {
1185 #doc_comment
1186 #[derive(Clone, Debug, PartialEq, Eq, Hash,)]
1187 pub struct #class_ident(dioxus_use_js::JsValue);
1188
1189 impl #class_ident {
1190 pub fn new(js_value: dioxus_use_js::JsValue) -> Self {
1191 Self(js_value)
1192 }
1193 }
1194
1195 impl #class_ident {
1196 #(#parts)*
1197 }
1198
1199 impl AsRef<dioxus_use_js::JsValue> for #class_ident {
1200 fn as_ref(&self) -> &dioxus_use_js::JsValue {
1201 &self.0
1202 }
1203 }
1204 }
1205}
1206
1207struct FunctionClassContext {
1208 class_name: String,
1209 ident: Ident,
1210 is_static: bool,
1211}
1212
1213fn generate_invocation(
1214 class: Option<FunctionClassContext>,
1215 func: &FunctionInfo,
1216 asset_path: &LitStr,
1217 function_id_hasher: &blake3::Hasher,
1218) -> TokenStream2 {
1219 let is_class_method = class.as_ref().is_some_and(|e| !e.is_static);
1220 let mut params = func.params.clone();
1221 if is_class_method {
1222 let new_param = ParamInfo {
1223 name: "_m_".to_owned(),
1224 js_type: None,
1225 rust_type: RustType::JsValue(JsValue {
1226 is_option: false,
1227 is_input: true,
1228 }),
1229 };
1230 params.insert(0, new_param);
1231 }
1232 let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
1234 let mut callback_name_to_info: IndexMap<String, &RustCallback> = IndexMap::new();
1235 let mut index: u64 = 0;
1236 let mut needs_drop = false;
1237 let mut has_callbacks = false;
1238 for param in ¶ms {
1239 if let RustType::Callback(callback) = ¶m.rust_type {
1240 callback_name_to_index.insert(param.name.to_owned(), index);
1241 index += 1;
1242 callback_name_to_info.insert(param.name.to_owned(), callback);
1243 has_callbacks = true;
1244 needs_drop = true;
1245 } else if param.is_drop() {
1246 needs_drop = true;
1247 }
1248 }
1249 let func_name_str = &func.name;
1250 let func_name_static_ident = quote! { FUNC_NAME };
1251
1252 let send_calls: Vec<TokenStream2> = params
1253 .iter()
1254 .flat_map(|param| {
1255 if param.is_drop() {
1256 return None;
1257 }
1258 let param_name = format_ident!("{}", param.name);
1259 match ¶m.rust_type {
1260 RustType::Regular(_) => Some(quote! {
1261 eval.send(#param_name).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1262 }),
1263 RustType::JsValue(js_value) => {
1264 if js_value.is_option {
1265 Some(quote! {
1266 #[allow(deprecated)]
1267 eval.send(#param_name.map(|e| e.internal_get())).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1268 })
1269 } else {
1270 Some(quote! {
1271 #[allow(deprecated)]
1272 eval.send(#param_name.internal_get()).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1273 })
1274 }
1275 },
1276 RustType::Callback(_) => {
1277 None
1278 },
1279 }
1280 })
1281 .collect();
1282
1283 let call_params = &func
1285 .params
1286 .iter()
1287 .map(|p| p.name.as_str())
1288 .collect::<Vec<&str>>()
1289 .join(", ");
1290 let prepare = if has_callbacks {
1291 assert!(needs_drop);
1292 "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;};"
1293 } else if needs_drop {
1294 "let _i_=\"**INVOCATION_ID**\";"
1295 } else {
1296 ""
1297 };
1298 let param_declarations = ¶ms
1299 .iter()
1300 .map(|param| {
1301 if needs_drop && param.is_drop() {
1302 return format!("let {}=_dp_;", param.name);
1303 }
1304 match ¶m.rust_type {
1305 RustType::Regular(_) => {
1306 format!("let {}=await dioxus.recv();", param.name)
1307 }
1308 RustType::JsValue(js_value) => {
1309 let param_name = ¶m.name;
1310 if js_value.is_option {
1311 format!(
1312 "let _{param_name}T_=await dioxus.recv();let {param_name}=null;if(_{param_name}T_!==null){{{param_name}=window[_{param_name}T_]}};",
1313 )
1314 }
1315 else {
1316 format!(
1317 "let _{param_name}T_=await dioxus.recv();let {param_name}=window[_{param_name}T_];",
1318 )
1319 }
1320 },
1321 RustType::Callback(rust_callback) => {
1322 let name = ¶m.name;
1323 let index = callback_name_to_index.get(name).unwrap();
1324 let RustCallback { input, output } = rust_callback;
1325 match (input, output) {
1326 (None, None) => {
1327 format!(
1329 "const {}=async()=>{{await _c_({},null);}};",
1330 name, index
1331 )
1332 },
1333 (None, Some(_)) => {
1334 format!(
1335 "const {}=async()=>{{return await _c_({},null);}};",
1336 name, index
1337
1338 )
1339 },
1340 (Some(_), None) => {
1341 format!(
1343 "const {}=async(v)=>{{await _c_({},v);}};",
1344 name, index
1345 )
1346 },
1347 (Some(_), Some(_)) => {
1348 format!(
1349 "const {}=async(v)=>{{return await _c_({},v);}};",
1350 name, index
1351 )
1352 },
1353 }
1354 },
1355 }})
1356 .collect::<Vec<_>>()
1357 .join("");
1358 let mut maybe_await = String::new();
1359 if func.is_async {
1360 maybe_await.push_str("await");
1361 }
1362 let func_call_full_path = if is_class_method {
1363 let var_name = ¶ms.first().unwrap().name;
1364 format!("{var_name}.{func_name_str}")
1365 } else if let Some(class) = &class {
1366 let class_name = &class.class_name;
1367 format!("{class_name}.{func_name_str}")
1368 } else {
1369 func_name_str.to_owned()
1370 };
1371 let call_function = match &func.rust_return_type {
1372 RustType::Regular(_) => {
1373 format!("return [true, {maybe_await} {func_call_full_path}({call_params})];")
1374 }
1375 RustType::Callback(_) => {
1376 unreachable!("This cannot be an output type, the macro should have panicked earlier.")
1377 }
1378 RustType::JsValue(js_value) => {
1379 let check = if js_value.is_option {
1380 "if (_v_===null||_v_===undefined){return [true,null];}".to_owned()
1382 } else {
1383 format!(
1384 "if (_v_===null||_v_===undefined){{console.error(\"The result of `{func_call_full_path}` was null or undefined, but a value is needed for JsValue\");return [true,null];}}"
1385 )
1386 };
1387 format!(
1388 "const _v_={maybe_await} {func_call_full_path}({call_params});{check}let _j_=\"__js-value-\"+crypto.randomUUID();window[_j_]=_v_;return [true,_j_];"
1389 )
1390 }
1391 };
1392 let drop_declare = if needs_drop {
1393 "let _d_;let _dp_=new Promise((r)=>_d_=r);window[_i_+\"d\"]=_d_;"
1395 } else {
1396 ""
1397 };
1398 let drop_handle = if needs_drop {
1399 if has_callbacks {
1400 "(async()=>{await _dp_;dioxus.close();_a_=false;let w=window[_i_];delete window[_i_];for(const[o, e] of Object.values(w)){e(new Error(\"Channel destroyed\"));}})();"
1401 } else {
1402 "(async()=>{await _dp_;dioxus.close();})();"
1403 }
1404 } else {
1405 assert!(
1406 !has_callbacks,
1407 "If this is true then needing drop should be true"
1408 );
1409 ""
1410 };
1411 let finally = if needs_drop {
1412 ""
1413 } else {
1414 "finally{dioxus.close();}"
1415 };
1416 let asset_path_string = asset_path.value();
1417 let js = if is_class_method {
1419 format!(
1420 "{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1421 )
1422 } else if let Some(class) = &class {
1423 let class_name = &class.class_name;
1424 format!(
1425 "const{{{class_name}}}=await import(\"{asset_path_string}\");{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1426 )
1427 } else {
1428 assert_eq!(func_call_full_path.as_str(), func_name_str);
1429 format!(
1430 "const{{{func_name_str}}}=await import(\"{asset_path_string}\");{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1431 )
1432 };
1433 fn to_raw_string_literal(s: &str) -> Literal {
1434 let mut hashes = String::from("#");
1435 while s.contains(&format!("\"{}", hashes)) {
1436 hashes.push('#');
1437 }
1438
1439 let raw = format!("r{h}\"{s}\"{h}", h = hashes);
1440 Literal::from_str(&raw).unwrap()
1441 }
1442 let comment = to_raw_string_literal(&js);
1443 let js_in_comment = quote! {
1445 #[doc = #comment]
1446 fn ___above_is_the_generated_js___() {}
1447 };
1448 let js_format = js.replace("{", "{{").replace("}", "}}");
1449 let js_format = if is_class_method {
1450 assert!(!js_format.contains(&asset_path_string));
1451 js_format
1452 } else {
1453 js_format.replace(&asset_path_string, "{}")
1454 };
1455 let js_format = if needs_drop {
1456 js_format.replace("**INVOCATION_ID**", "{}")
1457 } else {
1458 js_format
1459 };
1460 let js_eval_statement = if needs_drop {
1461 let js_line = if is_class_method {
1462 quote! {
1463 let js = format!(#js_format, &invocation_id);
1464 }
1465 } else {
1466 quote! {
1467 const MODULE: Asset = asset!(#asset_path);
1468 let js = format!(#js_format, MODULE, &invocation_id);
1469 }
1470 };
1471 let function_id = {
1472 let mut hasher = function_id_hasher.clone();
1473 hasher.update(func_call_full_path.as_bytes());
1474 let mut output_reader = hasher.finalize_xof();
1475 let mut truncated_bytes = vec![0u8; 10];
1476 use std::io::Read;
1477 output_reader.read_exact(&mut truncated_bytes).unwrap();
1478 let function_id =
1479 base64::engine::general_purpose::STANDARD_NO_PAD.encode(truncated_bytes);
1480 function_id
1481 };
1482 quote! {
1483 static INVOCATION_NUM: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1484 let invocation_id = format!("__{}{}", #function_id, INVOCATION_NUM.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
1486 #js_line
1487 let mut eval = dioxus::document::eval(js.as_str());
1488 }
1489 } else {
1490 if is_class_method {
1491 quote! {
1492 let js = #js_format;
1493 let mut eval = dioxus::document::eval(js);
1494 }
1495 } else {
1496 quote! {
1497 const MODULE: Asset = asset!(#asset_path);
1498 let js = format!(#js_format, MODULE);
1499 let mut eval = dioxus::document::eval(js.as_str());
1500 }
1501 }
1502 };
1503
1504 let param_types: Vec<_> = params
1506 .iter()
1507 .filter_map(|param| {
1508 if param.is_drop() {
1509 return None;
1510 }
1511 let param_name = format_ident!("{}", param.name);
1512 let type_tokens = param.rust_type.to_tokens();
1513 Some(quote! { #param_name: #type_tokens })
1514 })
1515 .collect();
1516
1517 let (return_type_tokens, generic_tokens) = return_type_tokens(
1518 &func.rust_return_type,
1519 func.ident.as_ref().map(|e| e.span()),
1520 );
1521
1522 let doc_comment = if func.doc_comment.is_empty() {
1524 quote! {}
1525 } else {
1526 let doc_lines: Vec<_> = func
1527 .doc_comment
1528 .iter()
1529 .map(|line| quote! { #[doc = #line] })
1530 .collect();
1531 quote! { #(#doc_lines)* }
1532 };
1533
1534 let func_name = func
1535 .ident
1536 .clone()
1537 .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
1539
1540 let void_output_mapping = if func.rust_return_type.to_string() == UNIT {
1542 quote! {
1543 .and_then(|e| {
1544 if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
1545 Ok(())
1546 } else {
1547 Err(dioxus_use_js::JsError::Eval {
1548 func: #func_name_static_ident,
1549 error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(
1550 <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1551 ))
1552 })
1553 }
1554 })
1555 }
1556 } else {
1557 quote! {}
1558 };
1559
1560 let callback_arms: Vec<TokenStream2> = callback_name_to_index
1561 .iter()
1562 .map(|(name, index)| {
1563 let callback_name = format_ident!("{}", name);
1564 let callback_info = callback_name_to_info.get(name).unwrap();
1565 let callback_call = match (&callback_info.input, &callback_info.output) {
1566 (None, None) => {
1567 quote! {
1568 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1569 let result = #callback_name(()).await;
1570
1571 match result {
1572 Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1574 Err(error) => responder.respond(request_id, false, error),
1575 }
1576 }});
1577 }
1578 },
1579 (None, Some(_)) => {
1580 quote! {
1581 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1582 let result = #callback_name(()).await;
1583
1584 match result {
1585 Ok(value) => responder.respond(request_id, true, value),
1586 Err(error) => responder.respond(request_id, false, error),
1587 }
1588 }});
1589 }
1590 },
1591 (Some(_), None) => {
1592 quote! {
1593 let value = values.next().unwrap();
1594 let value = match dioxus_use_js::serde_json_from_value(value) {
1595 Ok(value) => value,
1596 Err(value) => {
1597 responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1598 continue;
1599 }
1600 };
1601
1602 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1603 let result = #callback_name(value).await;
1604
1605 match result {
1606 Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1608 Err(error) => responder.respond(request_id, false, error),
1609 }
1610 }});
1611 }
1612 },
1613 (Some(_), Some(_)) => {
1614 quote! {
1615 let value = values.next().unwrap();
1616 let value = match dioxus_use_js::serde_json_from_value(value) {
1617 Ok(value) => value,
1618 Err(value) => {
1619 responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1620 continue;
1621 }
1622 };
1623
1624 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1625 let result = #callback_name(value).await;
1626
1627 match result {
1628 Ok(value) => responder.respond(request_id, true, value),
1629 Err(error) => responder.respond(request_id, false, error),
1630 }
1631 }});
1632 }
1633 }
1634 };
1635 quote! {
1636 #index => {
1637 #callback_call
1638 }
1639 }
1640 })
1641 .collect();
1642
1643 let callback_spawn = if !callback_arms.is_empty() {
1644 quote! {
1645 dioxus::prelude::spawn({
1646 async move {
1647 let responder = dioxus_use_js::CallbackResponder::new(&invocation_id);
1648 let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id.clone());
1649 loop {
1650 let result = eval.recv::<dioxus_use_js::SerdeJsonValue>().await;
1651 let value = match result {
1652 Ok(v) => v,
1653 Err(e) => {
1654 dioxus::prelude::error!(
1658 "Callback receiver errored. Shutting down all callbacks for invocation id `{}`: {:?}",
1659 &invocation_id,
1660 e
1661 );
1662 return;
1663 }
1664 };
1665 let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1666 unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1667 };
1668 let len = values.len();
1669 if len != 3 {
1670 unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1671 }
1672 let mut values = values.into_iter();
1673 let action = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1674 let request_id = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1675 match action {
1676 #(#callback_arms,)*
1677 _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1678 }
1679 }
1680 }
1681 });
1682 }
1683 } else if needs_drop {
1684 quote! {
1688 dioxus::prelude::spawn(async move {
1689 let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id);
1690 let f = dioxus_use_js::PendingFuture;
1691 f.await;
1692 });
1693 }
1694 } else {
1695 quote! {}
1696 };
1697
1698 let end_statement = quote! {
1699 let value = eval.await.map_err(|e| {
1700 dioxus_use_js::JsError::Eval {
1701 func: #func_name_static_ident,
1702 error: std::sync::Arc::new(e),
1703 }
1704 })?;
1705 let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1706 unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1707 };
1708 if values.len() != 2 {
1709 unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1710 }
1711 let mut values = values.into_iter();
1712 let success = values.next().unwrap().as_bool().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1713 if success {
1714 let value = values.next().unwrap();
1715 return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1716 dioxus_use_js::JsError::Eval {
1717 func: #func_name_static_ident,
1718 error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(e)),
1719 }
1720 })
1721 #void_output_mapping;
1722 } else {
1723 return Err(dioxus_use_js::JsError::Threw { func: #func_name_static_ident });
1724 }
1725 };
1726
1727 quote! {
1728 #doc_comment
1729 #[allow(non_snake_case)]
1730 pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1731 const #func_name_static_ident: &str = #func_name_str;
1732 #js_in_comment
1733 #js_eval_statement
1734 #(#send_calls)*
1735 #callback_spawn
1736 #end_statement
1737 }
1738 }
1739}
1740
1741fn return_type_tokens(
1742 return_type: &RustType,
1743 span: Option<proc_macro2::Span>,
1744) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
1745 let span = span.unwrap_or_else(|| proc_macro2::Span::call_site());
1746 let parsed_type = return_type.to_tokens();
1747 if return_type.to_string() == DEFAULT_GENERIC_OUTPUT {
1748 let generic = Ident::new(DEFAULT_GENERIC_OUTPUT, span);
1749 let generic_decl: TypeParam = syn::parse_str(DEFAULT_OUTPUT_GENERIC_DECLARTION).unwrap();
1750 (
1751 quote! { Result<#generic, dioxus_use_js::JsError> },
1752 Some(quote! { <#generic_decl> }),
1753 )
1754 } else {
1755 (
1756 quote! { Result<#parsed_type, dioxus_use_js::JsError> },
1757 None,
1758 )
1759 }
1760}
1761
1762#[proc_macro]
1764pub fn use_js(input: TokenStream) -> TokenStream {
1765 let input = parse_macro_input!(input as UseJsInput);
1766
1767 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1768 Ok(dir) => dir,
1769 Err(_) => {
1770 return TokenStream::from(
1771 syn::Error::new(
1772 proc_macro2::Span::call_site(),
1773 "CARGO_MANIFEST_DIR environment variable not found",
1774 )
1775 .to_compile_error(),
1776 );
1777 }
1778 };
1779
1780 let UseJsInput {
1781 js_bundle_path,
1782 ts_source_path,
1783 import_spec,
1784 } = input;
1785
1786 let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1787
1788 let (js_all_functions, js_all_classes) = match parse_script_file(&js_file_path, true) {
1789 Ok(result) => result,
1790 Err(e) => return TokenStream::from(e.to_compile_error()),
1791 };
1792
1793 let (js_classes_to_generate, js_functions_to_generate) = match get_types_to_generate(
1794 js_all_classes,
1795 js_all_functions,
1796 &import_spec,
1797 &js_file_path,
1798 ) {
1799 Ok((classes, funcs)) => (classes, funcs),
1800 Err(e) => {
1801 return TokenStream::from(e.to_compile_error());
1802 }
1803 };
1804
1805 let (functions_to_generate, classes_to_generate) = if let Some(ts_file_path) = ts_source_path {
1806 let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1807 let (ts_all_functions, ts_all_classes) = match parse_script_file(&ts_file_path, false) {
1808 Ok(result) => result,
1809 Err(e) => return TokenStream::from(e.to_compile_error()),
1810 };
1811
1812 let (ts_classes_to_generate, ts_functions_to_generate) = match get_types_to_generate(
1813 ts_all_classes,
1814 ts_all_functions,
1815 &import_spec,
1816 &ts_file_path,
1817 ) {
1818 Ok((classes, funcs)) => (classes, funcs),
1819 Err(e) => {
1820 return TokenStream::from(e.to_compile_error());
1821 }
1822 };
1823
1824 for ts_func in ts_functions_to_generate.iter() {
1825 if let Some(js_func) = js_functions_to_generate
1826 .iter()
1827 .find(|f| f.name == ts_func.name)
1828 {
1829 if ts_func.params.len() != js_func.params.len() {
1830 return TokenStream::from(syn::Error::new(
1831 proc_macro2::Span::call_site(),
1832 format!(
1833 "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1834 ts_func.name
1835 ),
1836 )
1837 .to_compile_error());
1838 }
1839 } else {
1840 return TokenStream::from(syn::Error::new(
1841 proc_macro2::Span::call_site(),
1842 format!(
1843 "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1844 ts_func.name
1845 ),
1846 )
1847 .to_compile_error());
1848 }
1849 }
1850
1851 for ts_class in ts_classes_to_generate.iter() {
1853 if let Some(js_class) = js_classes_to_generate
1854 .iter()
1855 .find(|c| c.name == ts_class.name)
1856 {
1857 if ts_class.methods.len() != js_class.methods.len() {
1858 return TokenStream::from(syn::Error::new(
1859 proc_macro2::Span::call_site(),
1860 format!(
1861 "Class '{}' has different method count in JS and TS files. Bundle may be out of date",
1862 ts_class.name
1863 ),
1864 )
1865 .to_compile_error());
1866 }
1867 } else {
1868 return TokenStream::from(syn::Error::new(
1869 proc_macro2::Span::call_site(),
1870 format!(
1871 "Class '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1872 ts_class.name
1873 ),
1874 )
1875 .to_compile_error());
1876 }
1877 }
1878
1879 (ts_functions_to_generate, ts_classes_to_generate)
1880 } else {
1881 (js_functions_to_generate, js_classes_to_generate)
1882 };
1883
1884 for function in functions_to_generate.iter() {
1885 for param in function.params.iter() {
1886 if param.name.starts_with("_") && param.name.ends_with("_") {
1887 panic!(
1888 "Parameter name '{}' in function '{}' is invalid. Parameters starting and ending with underscores are reserved.",
1889 param.name, function.name
1890 );
1891 }
1892 if param.name == "dioxus" {
1893 panic!(
1894 "Parameter name 'dioxus' in function '{}' is invalid. This parameter name is reserved.",
1895 function.name
1896 );
1897 }
1898 if param.name == function.name {
1899 panic!(
1900 "Parameter name '{}' in function '{}' is invalid. Parameters cannot have the same name as the function.",
1901 param.name, function.name
1902 );
1903 }
1904 }
1905 }
1906
1907 let call_site_span = proc_macro::Span::call_site();
1908 let file = call_site_span.file();
1909 let line_number = call_site_span.line();
1910 let column_number = call_site_span.column();
1911 let mut unhashed_id = file;
1912 unhashed_id.push_str(":");
1913 unhashed_id.push_str(&line_number.to_string());
1914 unhashed_id.push_str(":");
1915 unhashed_id.push_str(&column_number.to_string());
1916 unhashed_id.push_str(":");
1917 let mut function_id_hasher = blake3::Hasher::new();
1918 function_id_hasher.update(unhashed_id.as_bytes());
1919
1920 let function_wrappers: Vec<TokenStream2> = functions_to_generate
1921 .iter()
1922 .map(|func| generate_invocation(None, func, &js_bundle_path, &function_id_hasher))
1923 .collect();
1924
1925 let class_wrappers: Vec<TokenStream2> = classes_to_generate
1926 .iter()
1927 .map(|class| generate_class_wrapper(class, &js_bundle_path, &function_id_hasher))
1928 .collect();
1929
1930 let expanded = quote! {
1931 #(#function_wrappers)*
1932 #(#class_wrappers)*
1933 };
1934
1935 TokenStream::from(expanded)
1936}
1937
1938#[cfg(test)]
1941mod tests {
1942 use super::*;
1943
1944 #[test]
1945 fn test_primitives() {
1946 assert_eq!(
1947 ts_type_to_rust_type(Some("string"), false).to_string(),
1948 "String"
1949 );
1950 assert_eq!(
1951 ts_type_to_rust_type(Some("string"), true).to_string(),
1952 "&str"
1953 );
1954 assert_eq!(
1955 ts_type_to_rust_type(Some("number"), false).to_string(),
1956 "f64"
1957 );
1958 assert_eq!(
1959 ts_type_to_rust_type(Some("number"), true).to_string(),
1960 "f64"
1961 );
1962 assert_eq!(
1963 ts_type_to_rust_type(Some("boolean"), false).to_string(),
1964 "bool"
1965 );
1966 assert_eq!(
1967 ts_type_to_rust_type(Some("boolean"), true).to_string(),
1968 "bool"
1969 );
1970 }
1971
1972 #[test]
1973 fn test_nullable_primitives() {
1974 assert_eq!(
1975 ts_type_to_rust_type(Some("string | null"), true).to_string(),
1976 "Option<&str>"
1977 );
1978 assert_eq!(
1979 ts_type_to_rust_type(Some("string | null"), false).to_string(),
1980 "Option<String>"
1981 );
1982 assert_eq!(
1983 ts_type_to_rust_type(Some("number | null"), true).to_string(),
1984 "Option<f64>"
1985 );
1986 assert_eq!(
1987 ts_type_to_rust_type(Some("number | null"), false).to_string(),
1988 "Option<f64>"
1989 );
1990 assert_eq!(
1991 ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1992 "Option<bool>"
1993 );
1994 assert_eq!(
1995 ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1996 "Option<bool>"
1997 );
1998 }
1999
2000 #[test]
2001 fn test_arrays() {
2002 assert_eq!(
2003 ts_type_to_rust_type(Some("string[]"), true).to_string(),
2004 "&[String]"
2005 );
2006 assert_eq!(
2007 ts_type_to_rust_type(Some("string[]"), false).to_string(),
2008 "Vec<String>"
2009 );
2010 assert_eq!(
2011 ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
2012 "&[f64]"
2013 );
2014 assert_eq!(
2015 ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
2016 "Vec<f64>"
2017 );
2018 }
2019
2020 #[test]
2021 fn test_nullable_array_elements() {
2022 assert_eq!(
2023 ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
2024 "&[Option<String>]"
2025 );
2026 assert_eq!(
2027 ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
2028 "Vec<Option<String>>"
2029 );
2030 assert_eq!(
2031 ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
2032 "&[Option<f64>]"
2033 );
2034 assert_eq!(
2035 ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
2036 "Vec<Option<f64>>"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_nullable_array_itself() {
2042 assert_eq!(
2043 ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
2044 "Option<&[String]>"
2045 );
2046 assert_eq!(
2047 ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
2048 "Option<Vec<String>>"
2049 );
2050 assert_eq!(
2051 ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
2052 "Option<&[f64]>"
2053 );
2054 assert_eq!(
2055 ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
2056 "Option<Vec<f64>>"
2057 );
2058 }
2059
2060 #[test]
2061 fn test_nullable_array_and_elements() {
2062 assert_eq!(
2063 ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
2064 "Option<&[Option<String>]>"
2065 );
2066 assert_eq!(
2067 ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
2068 "Option<Vec<Option<String>>>"
2069 );
2070 }
2071
2072 #[test]
2073 fn test_fallback_for_union() {
2074 assert_eq!(
2075 ts_type_to_rust_type(Some("string | number"), true).to_string(),
2076 "impl dioxus_use_js::SerdeSerialize"
2077 );
2078 assert_eq!(
2079 ts_type_to_rust_type(Some("string | number"), false).to_string(),
2080 "DeserializeOwned"
2081 );
2082 assert_eq!(
2083 ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
2084 "impl dioxus_use_js::SerdeSerialize"
2085 );
2086 assert_eq!(
2087 ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
2088 "DeserializeOwned"
2089 );
2090 }
2091
2092 #[test]
2093 fn test_unknown_types() {
2094 assert_eq!(
2095 ts_type_to_rust_type(Some("foo"), true).to_string(),
2096 "impl dioxus_use_js::SerdeSerialize"
2097 );
2098 assert_eq!(
2099 ts_type_to_rust_type(Some("foo"), false).to_string(),
2100 "DeserializeOwned"
2101 );
2102
2103 assert_eq!(
2104 ts_type_to_rust_type(Some("any"), true).to_string(),
2105 "impl dioxus_use_js::SerdeSerialize"
2106 );
2107 assert_eq!(
2108 ts_type_to_rust_type(Some("any"), false).to_string(),
2109 "DeserializeOwned"
2110 );
2111 assert_eq!(
2112 ts_type_to_rust_type(Some("object"), true).to_string(),
2113 "impl dioxus_use_js::SerdeSerialize"
2114 );
2115 assert_eq!(
2116 ts_type_to_rust_type(Some("object"), false).to_string(),
2117 "DeserializeOwned"
2118 );
2119 assert_eq!(
2120 ts_type_to_rust_type(Some("unknown"), true).to_string(),
2121 "impl dioxus_use_js::SerdeSerialize"
2122 );
2123 assert_eq!(
2124 ts_type_to_rust_type(Some("unknown"), false).to_string(),
2125 "DeserializeOwned"
2126 );
2127
2128 assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
2129 assert_eq!(
2130 ts_type_to_rust_type(Some("undefined"), false).to_string(),
2131 "()"
2132 );
2133 assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
2134 }
2135
2136 #[test]
2137 fn test_extra_whitespace() {
2138 assert_eq!(
2139 ts_type_to_rust_type(Some(" string | null "), true).to_string(),
2140 "Option<&str>"
2141 );
2142 assert_eq!(
2143 ts_type_to_rust_type(Some(" string | null "), false).to_string(),
2144 "Option<String>"
2145 );
2146 assert_eq!(
2147 ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
2148 "&[String]"
2149 );
2150 assert_eq!(
2151 ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
2152 "Vec<String>"
2153 );
2154 }
2155
2156 #[test]
2157 fn test_map_types() {
2158 assert_eq!(
2159 ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
2160 "&std::collections::HashMap<String, f64>"
2161 );
2162 assert_eq!(
2163 ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
2164 "std::collections::HashMap<String, f64>"
2165 );
2166 assert_eq!(
2167 ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
2168 "&std::collections::HashMap<String, bool>"
2169 );
2170 assert_eq!(
2171 ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
2172 "std::collections::HashMap<String, bool>"
2173 );
2174 assert_eq!(
2175 ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
2176 "&std::collections::HashMap<f64, String>"
2177 );
2178 assert_eq!(
2179 ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
2180 "std::collections::HashMap<f64, String>"
2181 );
2182 }
2183
2184 #[test]
2185 fn test_set_types() {
2186 assert_eq!(
2187 ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
2188 "&std::collections::HashSet<String>"
2189 );
2190 assert_eq!(
2191 ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
2192 "std::collections::HashSet<String>"
2193 );
2194 assert_eq!(
2195 ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
2196 "&std::collections::HashSet<f64>"
2197 );
2198 assert_eq!(
2199 ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
2200 "std::collections::HashSet<f64>"
2201 );
2202 assert_eq!(
2203 ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
2204 "&std::collections::HashSet<bool>"
2205 );
2206 assert_eq!(
2207 ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
2208 "std::collections::HashSet<bool>"
2209 );
2210 }
2211
2212 #[test]
2213 fn test_rust_callback() {
2214 assert_eq!(
2215 ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
2216 "dioxus::core::Callback<f64, impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2217 );
2218 assert_eq!(
2219 ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
2220 "dioxus::core::Callback<(), impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2221 );
2222 assert_eq!(
2223 ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
2224 "dioxus::core::Callback<(), impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2225 );
2226 assert_eq!(
2227 ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
2228 "dioxus::core::Callback<f64, impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2229 );
2230 }
2231
2232 #[test]
2233 fn test_promise_types() {
2234 assert_eq!(
2235 ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
2236 "String"
2237 );
2238 assert_eq!(
2239 ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
2240 "f64"
2241 );
2242 assert_eq!(
2243 ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
2244 "bool"
2245 );
2246 }
2247
2248 #[test]
2249 fn test_json_types() {
2250 assert_eq!(
2251 ts_type_to_rust_type(Some("Json"), true).to_string(),
2252 "&dioxus_use_js::SerdeJsonValue"
2253 );
2254 assert_eq!(
2255 ts_type_to_rust_type(Some("Json"), false).to_string(),
2256 "dioxus_use_js::SerdeJsonValue"
2257 );
2258 }
2259
2260 #[test]
2261 fn test_js_value() {
2262 assert_eq!(
2263 ts_type_to_rust_type(Some("JsValue"), true).to_string(),
2264 "&dioxus_use_js::JsValue"
2265 );
2266 assert_eq!(
2267 ts_type_to_rust_type(Some("JsValue"), false).to_string(),
2268 "dioxus_use_js::JsValue"
2269 );
2270 assert_eq!(
2271 ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
2272 "&dioxus_use_js::JsValue"
2273 );
2274 assert_eq!(
2275 ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
2276 "dioxus_use_js::JsValue"
2277 );
2278
2279 assert_eq!(
2280 ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
2281 "dioxus_use_js::JsValue"
2282 );
2283
2284 assert_eq!(
2285 ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
2286 "Option<dioxus_use_js::JsValue>"
2287 );
2288 assert_eq!(
2289 ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
2290 "Option<&dioxus_use_js::JsValue>"
2291 );
2292 assert_eq!(
2293 ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
2294 "Option<dioxus_use_js::JsValue>"
2295 );
2296 }
2297
2298 #[test]
2299 fn test_class_parsing() {
2300 let ts_content = r#"
2301 /**
2302 * A test class
2303 */
2304 export class MyClass {
2305 constructor(name: string, value: number) {}
2306
2307 /**
2308 * Instance method
2309 */
2310 greet(greeting: string): string {
2311 return greeting;
2312 }
2313
2314 /**
2315 * Async method
2316 */
2317 async fetchData(url: string): Promise<string> {
2318 return "data";
2319 }
2320
2321 /**
2322 * Static method
2323 */
2324 static create(): MyClass {
2325 return new MyClass("test", 0);
2326 }
2327 }
2328 "#;
2329
2330 let source_map = SourceMap::default();
2331 let fm = source_map.new_source_file(
2332 swc_common::FileName::Custom("test.ts".to_string()).into(),
2333 ts_content.to_string(),
2334 );
2335 let comments = SingleThreadedComments::default();
2336
2337 let syntax = Syntax::Typescript(swc_ecma_parser::TsSyntax {
2338 tsx: false,
2339 decorators: false,
2340 dts: false,
2341 no_early_errors: false,
2342 disallow_ambiguous_jsx_like: true,
2343 });
2344
2345 let lexer = Lexer::new(
2346 syntax,
2347 Default::default(),
2348 StringInput::from(&*fm),
2349 Some(&comments),
2350 );
2351
2352 let mut parser = Parser::new_from(lexer);
2353 let module = parser.parse_module().unwrap();
2354
2355 let mut visitor = JsVisitor::new(comments, source_map);
2356 module.visit_with(&mut visitor);
2357
2358 visitor
2360 .classes
2361 .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
2362
2363 assert_eq!(visitor.classes.len(), 1);
2365 let class = &visitor.classes[0];
2366 assert_eq!(class.name, "MyClass");
2367 assert_eq!(class.is_exported, true);
2368
2369 assert_eq!(class.methods.len(), 3);
2371
2372 let greet = &class.methods[0];
2373 assert_eq!(greet.name, "greet");
2374 assert_eq!(greet.is_async, false);
2375 assert_eq!(greet.is_static, false);
2376 assert_eq!(greet.params.len(), 1);
2377 assert_eq!(greet.params[0].name, "greeting");
2378 assert_eq!(greet.params[0].rust_type.to_string(), "&str");
2379 assert_eq!(greet.rust_return_type.to_string(), "String");
2380
2381 let fetch_data = &class.methods[1];
2382 assert_eq!(fetch_data.name, "fetchData");
2383 assert_eq!(fetch_data.is_async, true);
2384 assert_eq!(fetch_data.is_static, false);
2385 assert_eq!(fetch_data.params.len(), 1);
2386 assert_eq!(fetch_data.rust_return_type.to_string(), "String");
2387
2388 let create = &class.methods[2];
2389 assert_eq!(create.name, "create");
2390 assert_eq!(create.is_async, false);
2391 assert_eq!(create.is_static, true);
2392 assert_eq!(create.params.len(), 0);
2393 }
2395}