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) {
808 let mut func = func.clone();
809 func.name = out_name.clone();
810 func.is_exported = true;
811 self.functions.push(func);
812 }
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 visitor
960 .functions
961 .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
962 visitor
963 .classes
964 .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
965 Ok((visitor.functions, visitor.classes))
966}
967
968fn get_types_to_generate(
969 classes: Vec<ClassInfo>,
970 functions: Vec<FunctionInfo>,
971 import_spec: &ImportSpec,
972 file: &Path,
973) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
974 fn named_helper(
975 names: &Vec<Ident>,
976 mut classes: Vec<ClassInfo>,
977 mut functions: Vec<FunctionInfo>,
978 file: &Path,
979 ) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
980 let mut funcs = Vec::new();
981 let mut classes = Vec::new();
982 for name in names {
983 let name_str = name.to_string();
984 if let Some(pos) = functions
985 .iter()
986 .position(|f: &FunctionInfo| f.name == name_str)
987 {
988 let mut function_info = functions.remove(pos);
989 if !function_info.is_exported {
990 return Err(syn::Error::new(
991 proc_macro2::Span::call_site(),
992 format!(
993 "Function '{}' not exported in file '{}'",
994 name,
995 file.display()
996 ),
997 ));
998 }
999 function_info.ident.replace(name.clone());
1000 funcs.push(function_info);
1001 } else if let Some(pos) = classes.iter().position(|c: &ClassInfo| c.name == name_str) {
1002 let mut class_info = classes.remove(pos);
1003 if !class_info.is_exported {
1004 return Err(syn::Error::new(
1005 proc_macro2::Span::call_site(),
1006 format!("Class '{}' not exported in file '{}'", name, file.display()),
1007 ));
1008 }
1009 class_info.ident.replace(name.clone());
1010 classes.push(class_info);
1011 } else {
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 }
1022 Ok((classes, funcs))
1023 }
1024 match import_spec {
1025 ImportSpec::All => Ok((
1026 classes.into_iter().filter(|e| e.is_exported).collect(),
1027 functions.into_iter().filter(|e| e.is_exported).collect(),
1028 )),
1029 ImportSpec::Single(name) => named_helper(&vec![name.clone()], classes, functions, file),
1030 ImportSpec::Named(names) => named_helper(names, classes, functions, file),
1031 }
1032}
1033
1034fn generate_class_wrapper(
1035 class_info: &ClassInfo,
1036 asset_path: &LitStr,
1037 function_id_hasher: &blake3::Hasher,
1038) -> TokenStream2 {
1039 let class_ident = class_info
1040 .ident
1041 .clone()
1042 .unwrap_or_else(|| Ident::new(class_info.name.as_str(), proc_macro2::Span::call_site()));
1043
1044 let doc_comment = if class_info.doc_comment.is_empty() {
1045 quote! {}
1046 } else {
1047 let doc_lines: Vec<_> = class_info
1048 .doc_comment
1049 .iter()
1050 .map(|line| quote! { #[doc = #line] })
1051 .collect();
1052 quote! { #(#doc_lines)* }
1053 };
1054
1055 let mut parts: Vec<TokenStream2> = Vec::new();
1056 for method in &class_info.methods {
1057 let func_info = FunctionInfo {
1058 name: method.name.clone(),
1059 ident: None,
1060 params: method.params.clone(),
1061 js_return_type: method.js_return_type.clone(),
1062 rust_return_type: method.rust_return_type.clone(),
1063 is_exported: true,
1064 is_async: method.is_async,
1065 doc_comment: method.doc_comment.clone(),
1066 };
1067
1068 let inner_function = generate_invocation(
1069 Some(FunctionClassContext {
1070 class_name: class_info.name.clone(),
1071 ident: class_ident.clone(),
1072 is_static: method.is_static,
1073 }),
1074 &func_info,
1075 asset_path,
1076 function_id_hasher,
1077 );
1078
1079 let method_name = format_ident!("{}", method.name);
1080 let method_params: Vec<_> = method
1081 .params
1082 .iter()
1083 .filter_map(|param| {
1084 if param.is_drop() {
1085 return None;
1086 }
1087 let param_name = format_ident!("{}", param.name);
1088 let type_tokens = param.rust_type.to_tokens();
1089 Some(quote! { #param_name: #type_tokens })
1090 })
1091 .collect();
1092
1093 let param_names: Vec<_> = method
1094 .params
1095 .iter()
1096 .map(|p| format_ident!("{}", p.name))
1097 .collect();
1098
1099 let method_doc = if method.doc_comment.is_empty() {
1100 quote! {}
1101 } else {
1102 let doc_lines: Vec<_> = method
1103 .doc_comment
1104 .iter()
1105 .map(|line| quote! { #[doc = #line] })
1106 .collect();
1107 quote! { #(#doc_lines)* }
1108 };
1109
1110 fn returns_self_type(func_info: &FunctionInfo, class_info: &ClassInfo) -> bool {
1111 let Some(js_return_type) = &func_info.js_return_type else {
1112 return false;
1113 };
1114 if !matches!(func_info.rust_return_type, RustType::JsValue(_)) {
1115 return false;
1116 }
1117 if js_return_type.starts_with("JsValue<") && js_return_type.ends_with('>') {
1118 let inner = js_return_type[8..js_return_type.len() - 1].trim();
1119 if inner == class_info.name {
1120 return true;
1121 }
1122 }
1123 return false;
1124 }
1125
1126 let (invocation, return_type, generic) = if returns_self_type(&func_info, &class_info) {
1127 let invocation = if method.is_static {
1129 quote! {
1130 Ok(#class_ident::new(#method_name(#(#param_names),*).await?))
1131 }
1132 } else {
1133 quote! {
1134 Ok(#class_ident::new(#method_name(&self.0, #(#param_names),*).await?))
1135 }
1136 };
1137 let (_, generic_tokens) = return_type_tokens(
1138 &method.rust_return_type,
1139 class_info.ident.as_ref().map(|e| e.span()),
1140 );
1141 let return_type_tokens = quote! { Result<#class_ident, dioxus_use_js::JsError> };
1142 (invocation, return_type_tokens, generic_tokens)
1143 } else {
1144 let invocation = if method.is_static {
1145 quote! {
1146 #method_name(#(#param_names),*).await
1147 }
1148 } else {
1149 quote! {
1150 #method_name(&self.0, #(#param_names),*).await
1151 }
1152 };
1153 let (return_type_tokens, generic_tokens) = return_type_tokens(
1154 &method.rust_return_type,
1155 class_info.ident.as_ref().map(|e| e.span()),
1156 );
1157 (invocation, return_type_tokens, generic_tokens)
1158 };
1159
1160 let part = if method.is_static {
1161 quote! {
1162 #method_doc
1163 #[allow(non_snake_case)]
1164 pub async fn #method_name #generic(#(#method_params),*) -> #return_type {
1165 #[inline]
1166 #inner_function
1167 #invocation
1168 }
1169 }
1170 } else {
1171 quote! {
1172 #method_doc
1173 #[allow(non_snake_case)]
1174 pub async fn #method_name #generic(&self, #(#method_params),*) -> #return_type {
1175 #[inline]
1176 #inner_function
1177 #invocation
1178 }
1179 }
1180 };
1181
1182 parts.push(part);
1183 }
1184
1185 quote! {
1186 #doc_comment
1187 #[derive(Clone, Debug, PartialEq, Eq, Hash,)]
1188 pub struct #class_ident(dioxus_use_js::JsValue);
1189
1190 impl #class_ident {
1191 pub fn new(js_value: dioxus_use_js::JsValue) -> Self {
1192 Self(js_value)
1193 }
1194 }
1195
1196 impl #class_ident {
1197 #(#parts)*
1198 }
1199
1200 impl AsRef<dioxus_use_js::JsValue> for #class_ident {
1201 fn as_ref(&self) -> &dioxus_use_js::JsValue {
1202 &self.0
1203 }
1204 }
1205 }
1206}
1207
1208struct FunctionClassContext {
1209 class_name: String,
1210 ident: Ident,
1211 is_static: bool,
1212}
1213
1214fn generate_invocation(
1215 class: Option<FunctionClassContext>,
1216 func: &FunctionInfo,
1217 asset_path: &LitStr,
1218 function_id_hasher: &blake3::Hasher,
1219) -> TokenStream2 {
1220 let is_class_method = class.as_ref().is_some_and(|e| !e.is_static);
1221 let mut params = func.params.clone();
1222 if is_class_method {
1223 let new_param = ParamInfo {
1224 name: "_m_".to_owned(),
1225 js_type: None,
1226 rust_type: RustType::JsValue(JsValue {
1227 is_option: false,
1228 is_input: true,
1229 }),
1230 };
1231 params.insert(0, new_param);
1232 }
1233 let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
1235 let mut callback_name_to_info: IndexMap<String, &RustCallback> = IndexMap::new();
1236 let mut index: u64 = 0;
1237 let mut needs_drop = false;
1238 let mut has_callbacks = false;
1239 for param in ¶ms {
1240 if let RustType::Callback(callback) = ¶m.rust_type {
1241 callback_name_to_index.insert(param.name.to_owned(), index);
1242 index += 1;
1243 callback_name_to_info.insert(param.name.to_owned(), callback);
1244 has_callbacks = true;
1245 needs_drop = true;
1246 } else if param.is_drop() {
1247 needs_drop = true;
1248 }
1249 }
1250 let func_name_str = &func.name;
1251 let func_name_static_ident = quote! { FUNC_NAME };
1252
1253 let send_calls: Vec<TokenStream2> = params
1254 .iter()
1255 .flat_map(|param| {
1256 if param.is_drop() {
1257 return None;
1258 }
1259 let param_name = format_ident!("{}", param.name);
1260 match ¶m.rust_type {
1261 RustType::Regular(_) => Some(quote! {
1262 eval.send(#param_name).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1263 }),
1264 RustType::JsValue(js_value) => {
1265 if js_value.is_option {
1266 Some(quote! {
1267 #[allow(deprecated)]
1268 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) })?;
1269 })
1270 } else {
1271 Some(quote! {
1272 #[allow(deprecated)]
1273 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) })?;
1274 })
1275 }
1276 },
1277 RustType::Callback(_) => {
1278 None
1279 },
1280 }
1281 })
1282 .collect();
1283
1284 let call_params = &func
1286 .params
1287 .iter()
1288 .map(|p| p.name.as_str())
1289 .collect::<Vec<&str>>()
1290 .join(", ");
1291 let prepare = if has_callbacks {
1292 assert!(needs_drop);
1293 "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;};"
1294 } else if needs_drop {
1295 "let _i_=\"**INVOCATION_ID**\";"
1296 } else {
1297 ""
1298 };
1299 let param_declarations = ¶ms
1300 .iter()
1301 .map(|param| {
1302 if needs_drop && param.is_drop() {
1303 return format!("let {}=_dp_;", param.name);
1304 }
1305 match ¶m.rust_type {
1306 RustType::Regular(_) => {
1307 format!("let {}=await dioxus.recv();", param.name)
1308 }
1309 RustType::JsValue(js_value) => {
1310 let param_name = ¶m.name;
1311 if js_value.is_option {
1312 format!(
1313 "let _{param_name}T_=await dioxus.recv();let {param_name}=null;if(_{param_name}T_!==null){{{param_name}=window[_{param_name}T_]}};",
1314 )
1315 }
1316 else {
1317 format!(
1318 "let _{param_name}T_=await dioxus.recv();let {param_name}=window[_{param_name}T_];",
1319 )
1320 }
1321 },
1322 RustType::Callback(rust_callback) => {
1323 let name = ¶m.name;
1324 let index = callback_name_to_index.get(name).unwrap();
1325 let RustCallback { input, output } = rust_callback;
1326 match (input, output) {
1327 (None, None) => {
1328 format!(
1330 "const {}=async()=>{{await _c_({},null);}};",
1331 name, index
1332 )
1333 },
1334 (None, Some(_)) => {
1335 format!(
1336 "const {}=async()=>{{return await _c_({},null);}};",
1337 name, index
1338
1339 )
1340 },
1341 (Some(_), None) => {
1342 format!(
1344 "const {}=async(v)=>{{await _c_({},v);}};",
1345 name, index
1346 )
1347 },
1348 (Some(_), Some(_)) => {
1349 format!(
1350 "const {}=async(v)=>{{return await _c_({},v);}};",
1351 name, index
1352 )
1353 },
1354 }
1355 },
1356 }})
1357 .collect::<Vec<_>>()
1358 .join("");
1359 let mut maybe_await = String::new();
1360 if func.is_async {
1361 maybe_await.push_str("await");
1362 }
1363 let func_call_full_path = if is_class_method {
1364 let var_name = ¶ms.first().unwrap().name;
1365 format!("{var_name}.{func_name_str}")
1366 } else if let Some(class) = &class {
1367 let class_name = &class.class_name;
1368 format!("{class_name}.{func_name_str}")
1369 } else {
1370 func_name_str.to_owned()
1371 };
1372 let call_function = match &func.rust_return_type {
1373 RustType::Regular(_) => {
1374 format!("return [true, {maybe_await} {func_call_full_path}({call_params})];")
1375 }
1376 RustType::Callback(_) => {
1377 unreachable!("This cannot be an output type, the macro should have panicked earlier.")
1378 }
1379 RustType::JsValue(js_value) => {
1380 let check = if js_value.is_option {
1381 "if (_v_===null||_v_===undefined){return [true,null];}".to_owned()
1383 } else {
1384 format!(
1385 "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];}}"
1386 )
1387 };
1388 format!(
1389 "const _v_={maybe_await} {func_call_full_path}({call_params});{check}let _j_=\"__js-value-\"+crypto.randomUUID();window[_j_]=_v_;return [true,_j_];"
1390 )
1391 }
1392 };
1393 let drop_declare = if needs_drop {
1394 "let _d_;let _dp_=new Promise((r)=>_d_=r);window[_i_+\"d\"]=_d_;"
1396 } else {
1397 ""
1398 };
1399 let drop_handle = if needs_drop {
1400 if has_callbacks {
1401 "(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\"));}})();"
1402 } else {
1403 "(async()=>{await _dp_;dioxus.close();})();"
1404 }
1405 } else {
1406 assert!(
1407 !has_callbacks,
1408 "If this is true then needing drop should be true"
1409 );
1410 ""
1411 };
1412 let finally = if needs_drop {
1413 ""
1414 } else {
1415 "finally{dioxus.close();}"
1416 };
1417 let asset_path_string = asset_path.value();
1418 let js = if is_class_method {
1420 format!(
1421 "{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}"
1422 )
1423 } else if let Some(class) = &class {
1424 let class_name = &class.class_name;
1425 format!(
1426 "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}"
1427 )
1428 } else {
1429 assert_eq!(func_call_full_path.as_str(), func_name_str);
1430 format!(
1431 "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}"
1432 )
1433 };
1434 fn to_raw_string_literal(s: &str) -> Literal {
1435 let mut hashes = String::from("#");
1436 while s.contains(&format!("\"{}", hashes)) {
1437 hashes.push('#');
1438 }
1439
1440 let raw = format!("r{h}\"{s}\"{h}", h = hashes);
1441 Literal::from_str(&raw).unwrap()
1442 }
1443 let comment = to_raw_string_literal(&js);
1444 let js_in_comment = quote! {
1446 #[doc = #comment]
1447 fn ___above_is_the_generated_js___() {}
1448 };
1449 let js_format = js.replace("{", "{{").replace("}", "}}");
1450 let js_format = if is_class_method {
1451 assert!(!js_format.contains(&asset_path_string));
1452 js_format
1453 } else {
1454 js_format.replace(&asset_path_string, "{}")
1455 };
1456 let js_format = if needs_drop {
1457 js_format.replace("**INVOCATION_ID**", "{}")
1458 } else {
1459 js_format
1460 };
1461 let js_eval_statement = if needs_drop {
1462 let js_line = if is_class_method {
1463 quote! {
1464 let js = format!(#js_format, &invocation_id);
1465 }
1466 } else {
1467 quote! {
1468 const MODULE: Asset = asset!(#asset_path);
1469 let js = format!(#js_format, MODULE, &invocation_id);
1470 }
1471 };
1472 let function_id = {
1473 let mut hasher = function_id_hasher.clone();
1474 hasher.update(func_call_full_path.as_bytes());
1475 let mut output_reader = hasher.finalize_xof();
1476 let mut truncated_bytes = vec![0u8; 10];
1477 use std::io::Read;
1478 output_reader.read_exact(&mut truncated_bytes).unwrap();
1479 let function_id =
1480 base64::engine::general_purpose::STANDARD_NO_PAD.encode(truncated_bytes);
1481 function_id
1482 };
1483 quote! {
1484 static INVOCATION_NUM: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1485 let invocation_id = format!("__{}{}", #function_id, INVOCATION_NUM.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
1487 #js_line
1488 let mut eval = dioxus::document::eval(js.as_str());
1489 }
1490 } else {
1491 if is_class_method {
1492 quote! {
1493 let js = #js_format;
1494 let mut eval = dioxus::document::eval(js);
1495 }
1496 } else {
1497 quote! {
1498 const MODULE: Asset = asset!(#asset_path);
1499 let js = format!(#js_format, MODULE);
1500 let mut eval = dioxus::document::eval(js.as_str());
1501 }
1502 }
1503 };
1504
1505 let param_types: Vec<_> = params
1507 .iter()
1508 .filter_map(|param| {
1509 if param.is_drop() {
1510 return None;
1511 }
1512 let param_name = format_ident!("{}", param.name);
1513 let type_tokens = param.rust_type.to_tokens();
1514 Some(quote! { #param_name: #type_tokens })
1515 })
1516 .collect();
1517
1518 let (return_type_tokens, generic_tokens) = return_type_tokens(
1519 &func.rust_return_type,
1520 func.ident.as_ref().map(|e| e.span()),
1521 );
1522
1523 let doc_comment = if func.doc_comment.is_empty() {
1525 quote! {}
1526 } else {
1527 let doc_lines: Vec<_> = func
1528 .doc_comment
1529 .iter()
1530 .map(|line| quote! { #[doc = #line] })
1531 .collect();
1532 quote! { #(#doc_lines)* }
1533 };
1534
1535 let func_name = func
1536 .ident
1537 .clone()
1538 .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
1540
1541 let void_output_mapping = if func.rust_return_type.to_string() == UNIT {
1543 quote! {
1544 .and_then(|e| {
1545 if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
1546 Ok(())
1547 } else {
1548 Err(dioxus_use_js::JsError::Eval {
1549 func: #func_name_static_ident,
1550 error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(
1551 <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1552 ))
1553 })
1554 }
1555 })
1556 }
1557 } else {
1558 quote! {}
1559 };
1560
1561 let callback_arms: Vec<TokenStream2> = callback_name_to_index
1562 .iter()
1563 .map(|(name, index)| {
1564 let callback_name = format_ident!("{}", name);
1565 let callback_info = callback_name_to_info.get(name).unwrap();
1566 let callback_call = match (&callback_info.input, &callback_info.output) {
1567 (None, None) => {
1568 quote! {
1569 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1570 let result = #callback_name(()).await;
1571
1572 match result {
1573 Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1575 Err(error) => responder.respond(request_id, false, error),
1576 }
1577 }});
1578 }
1579 },
1580 (None, Some(_)) => {
1581 quote! {
1582 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1583 let result = #callback_name(()).await;
1584
1585 match result {
1586 Ok(value) => responder.respond(request_id, true, value),
1587 Err(error) => responder.respond(request_id, false, error),
1588 }
1589 }});
1590 }
1591 },
1592 (Some(_), None) => {
1593 quote! {
1594 let value = values.next().unwrap();
1595 let value = match dioxus_use_js::serde_json_from_value(value) {
1596 Ok(value) => value,
1597 Err(value) => {
1598 responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1599 continue;
1600 }
1601 };
1602
1603 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1604 let result = #callback_name(value).await;
1605
1606 match result {
1607 Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1609 Err(error) => responder.respond(request_id, false, error),
1610 }
1611 }});
1612 }
1613 },
1614 (Some(_), Some(_)) => {
1615 quote! {
1616 let value = values.next().unwrap();
1617 let value = match dioxus_use_js::serde_json_from_value(value) {
1618 Ok(value) => value,
1619 Err(value) => {
1620 responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1621 continue;
1622 }
1623 };
1624
1625 dioxus::prelude::spawn({let responder = responder.clone(); async move {
1626 let result = #callback_name(value).await;
1627
1628 match result {
1629 Ok(value) => responder.respond(request_id, true, value),
1630 Err(error) => responder.respond(request_id, false, error),
1631 }
1632 }});
1633 }
1634 }
1635 };
1636 quote! {
1637 #index => {
1638 #callback_call
1639 }
1640 }
1641 })
1642 .collect();
1643
1644 let callback_spawn = if !callback_arms.is_empty() {
1645 quote! {
1646 dioxus::prelude::spawn({
1647 async move {
1648 let responder = dioxus_use_js::CallbackResponder::new(&invocation_id);
1649 let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id.clone());
1650 loop {
1651 let result = eval.recv::<dioxus_use_js::SerdeJsonValue>().await;
1652 let value = match result {
1653 Ok(v) => v,
1654 Err(e) => {
1655 dioxus::prelude::error!(
1659 "Callback receiver errored. Shutting down all callbacks for invocation id `{}`: {:?}",
1660 &invocation_id,
1661 e
1662 );
1663 return;
1664 }
1665 };
1666 let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1667 unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1668 };
1669 let len = values.len();
1670 if len != 3 {
1671 unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1672 }
1673 let mut values = values.into_iter();
1674 let action = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1675 let request_id = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1676 match action {
1677 #(#callback_arms,)*
1678 _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1679 }
1680 }
1681 }
1682 });
1683 }
1684 } else if needs_drop {
1685 quote! {
1689 dioxus::prelude::spawn(async move {
1690 let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id);
1691 let f = dioxus_use_js::PendingFuture;
1692 f.await;
1693 });
1694 }
1695 } else {
1696 quote! {}
1697 };
1698
1699 let end_statement = quote! {
1700 let value = eval.await.map_err(|e| {
1701 dioxus_use_js::JsError::Eval {
1702 func: #func_name_static_ident,
1703 error: std::sync::Arc::new(e),
1704 }
1705 })?;
1706 let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1707 unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1708 };
1709 if values.len() != 2 {
1710 unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1711 }
1712 let mut values = values.into_iter();
1713 let success = values.next().unwrap().as_bool().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1714 if success {
1715 let value = values.next().unwrap();
1716 return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1717 dioxus_use_js::JsError::Eval {
1718 func: #func_name_static_ident,
1719 error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(e)),
1720 }
1721 })
1722 #void_output_mapping;
1723 } else {
1724 return Err(dioxus_use_js::JsError::Threw { func: #func_name_static_ident });
1725 }
1726 };
1727
1728 quote! {
1729 #doc_comment
1730 #[allow(non_snake_case)]
1731 pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1732 const #func_name_static_ident: &str = #func_name_str;
1733 #js_in_comment
1734 #js_eval_statement
1735 #(#send_calls)*
1736 #callback_spawn
1737 #end_statement
1738 }
1739 }
1740}
1741
1742fn return_type_tokens(
1743 return_type: &RustType,
1744 span: Option<proc_macro2::Span>,
1745) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
1746 let span = span.unwrap_or_else(|| proc_macro2::Span::call_site());
1747 let parsed_type = return_type.to_tokens();
1748 if return_type.to_string() == DEFAULT_GENERIC_OUTPUT {
1749 let generic = Ident::new(DEFAULT_GENERIC_OUTPUT, span);
1750 let generic_decl: TypeParam = syn::parse_str(DEFAULT_OUTPUT_GENERIC_DECLARTION).unwrap();
1751 (
1752 quote! { Result<#generic, dioxus_use_js::JsError> },
1753 Some(quote! { <#generic_decl> }),
1754 )
1755 } else {
1756 (
1757 quote! { Result<#parsed_type, dioxus_use_js::JsError> },
1758 None,
1759 )
1760 }
1761}
1762
1763#[proc_macro]
1765pub fn use_js(input: TokenStream) -> TokenStream {
1766 let input = parse_macro_input!(input as UseJsInput);
1767
1768 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1769 Ok(dir) => dir,
1770 Err(_) => {
1771 return TokenStream::from(
1772 syn::Error::new(
1773 proc_macro2::Span::call_site(),
1774 "CARGO_MANIFEST_DIR environment variable not found",
1775 )
1776 .to_compile_error(),
1777 );
1778 }
1779 };
1780
1781 let UseJsInput {
1782 js_bundle_path,
1783 ts_source_path,
1784 import_spec,
1785 } = input;
1786
1787 let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1788
1789 let (js_all_functions, js_all_classes) = match parse_script_file(&js_file_path, true) {
1790 Ok(result) => result,
1791 Err(e) => return TokenStream::from(e.to_compile_error()),
1792 };
1793
1794 let (js_classes_to_generate, js_functions_to_generate) = match get_types_to_generate(
1795 js_all_classes,
1796 js_all_functions,
1797 &import_spec,
1798 &js_file_path,
1799 ) {
1800 Ok((classes, funcs)) => (classes, funcs),
1801 Err(e) => {
1802 return TokenStream::from(e.to_compile_error());
1803 }
1804 };
1805
1806 let (functions_to_generate, classes_to_generate) = if let Some(ts_file_path) = ts_source_path {
1807 let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1808 let (ts_all_functions, ts_all_classes) = match parse_script_file(&ts_file_path, false) {
1809 Ok(result) => result,
1810 Err(e) => return TokenStream::from(e.to_compile_error()),
1811 };
1812
1813 let (ts_classes_to_generate, ts_functions_to_generate) = match get_types_to_generate(
1814 ts_all_classes,
1815 ts_all_functions,
1816 &import_spec,
1817 &ts_file_path,
1818 ) {
1819 Ok((classes, funcs)) => (classes, funcs),
1820 Err(e) => {
1821 return TokenStream::from(e.to_compile_error());
1822 }
1823 };
1824
1825 for ts_func in ts_functions_to_generate.iter() {
1826 if let Some(js_func) = js_functions_to_generate
1827 .iter()
1828 .find(|f| f.name == ts_func.name)
1829 {
1830 if ts_func.params.len() != js_func.params.len() {
1831 return TokenStream::from(syn::Error::new(
1832 proc_macro2::Span::call_site(),
1833 format!(
1834 "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1835 ts_func.name
1836 ),
1837 )
1838 .to_compile_error());
1839 }
1840 } else {
1841 return TokenStream::from(syn::Error::new(
1842 proc_macro2::Span::call_site(),
1843 format!(
1844 "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1845 ts_func.name
1846 ),
1847 )
1848 .to_compile_error());
1849 }
1850 }
1851
1852 for ts_class in ts_classes_to_generate.iter() {
1854 if let Some(js_class) = js_classes_to_generate
1855 .iter()
1856 .find(|c| c.name == ts_class.name)
1857 {
1858 if ts_class.methods.len() != js_class.methods.len() {
1859 return TokenStream::from(syn::Error::new(
1860 proc_macro2::Span::call_site(),
1861 format!(
1862 "Class '{}' has different method count in JS and TS files. Bundle may be out of date",
1863 ts_class.name
1864 ),
1865 )
1866 .to_compile_error());
1867 }
1868 } else {
1869 return TokenStream::from(syn::Error::new(
1870 proc_macro2::Span::call_site(),
1871 format!(
1872 "Class '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1873 ts_class.name
1874 ),
1875 )
1876 .to_compile_error());
1877 }
1878 }
1879
1880 (ts_functions_to_generate, ts_classes_to_generate)
1881 } else {
1882 (js_functions_to_generate, js_classes_to_generate)
1883 };
1884
1885 for function in functions_to_generate.iter() {
1886 for param in function.params.iter() {
1887 if param.name.starts_with("_") && param.name.ends_with("_") {
1888 panic!(
1889 "Parameter name '{}' in function '{}' is invalid. Parameters starting and ending with underscores are reserved.",
1890 param.name, function.name
1891 );
1892 }
1893 if param.name == "dioxus" {
1894 panic!(
1895 "Parameter name 'dioxus' in function '{}' is invalid. This parameter name is reserved.",
1896 function.name
1897 );
1898 }
1899 if param.name == function.name {
1900 panic!(
1901 "Parameter name '{}' in function '{}' is invalid. Parameters cannot have the same name as the function.",
1902 param.name, function.name
1903 );
1904 }
1905 }
1906 }
1907
1908 let call_site_span = proc_macro::Span::call_site();
1909 let file = call_site_span.file();
1910 let line_number = call_site_span.line();
1911 let column_number = call_site_span.column();
1912 let mut unhashed_id = file;
1913 unhashed_id.push_str(":");
1914 unhashed_id.push_str(&line_number.to_string());
1915 unhashed_id.push_str(":");
1916 unhashed_id.push_str(&column_number.to_string());
1917 unhashed_id.push_str(":");
1918 let mut function_id_hasher = blake3::Hasher::new();
1919 function_id_hasher.update(unhashed_id.as_bytes());
1920
1921 let function_wrappers: Vec<TokenStream2> = functions_to_generate
1922 .iter()
1923 .map(|func| generate_invocation(None, func, &js_bundle_path, &function_id_hasher))
1924 .collect();
1925
1926 let class_wrappers: Vec<TokenStream2> = classes_to_generate
1927 .iter()
1928 .map(|class| generate_class_wrapper(class, &js_bundle_path, &function_id_hasher))
1929 .collect();
1930
1931 let expanded = quote! {
1932 #(#function_wrappers)*
1933 #(#class_wrappers)*
1934 };
1935
1936 TokenStream::from(expanded)
1937}
1938
1939#[cfg(test)]
1942mod tests {
1943 use super::*;
1944
1945 #[test]
1946 fn test_primitives() {
1947 assert_eq!(
1948 ts_type_to_rust_type(Some("string"), false).to_string(),
1949 "String"
1950 );
1951 assert_eq!(
1952 ts_type_to_rust_type(Some("string"), true).to_string(),
1953 "&str"
1954 );
1955 assert_eq!(
1956 ts_type_to_rust_type(Some("number"), false).to_string(),
1957 "f64"
1958 );
1959 assert_eq!(
1960 ts_type_to_rust_type(Some("number"), true).to_string(),
1961 "f64"
1962 );
1963 assert_eq!(
1964 ts_type_to_rust_type(Some("boolean"), false).to_string(),
1965 "bool"
1966 );
1967 assert_eq!(
1968 ts_type_to_rust_type(Some("boolean"), true).to_string(),
1969 "bool"
1970 );
1971 }
1972
1973 #[test]
1974 fn test_nullable_primitives() {
1975 assert_eq!(
1976 ts_type_to_rust_type(Some("string | null"), true).to_string(),
1977 "Option<&str>"
1978 );
1979 assert_eq!(
1980 ts_type_to_rust_type(Some("string | null"), false).to_string(),
1981 "Option<String>"
1982 );
1983 assert_eq!(
1984 ts_type_to_rust_type(Some("number | null"), true).to_string(),
1985 "Option<f64>"
1986 );
1987 assert_eq!(
1988 ts_type_to_rust_type(Some("number | null"), false).to_string(),
1989 "Option<f64>"
1990 );
1991 assert_eq!(
1992 ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1993 "Option<bool>"
1994 );
1995 assert_eq!(
1996 ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1997 "Option<bool>"
1998 );
1999 }
2000
2001 #[test]
2002 fn test_arrays() {
2003 assert_eq!(
2004 ts_type_to_rust_type(Some("string[]"), true).to_string(),
2005 "&[String]"
2006 );
2007 assert_eq!(
2008 ts_type_to_rust_type(Some("string[]"), false).to_string(),
2009 "Vec<String>"
2010 );
2011 assert_eq!(
2012 ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
2013 "&[f64]"
2014 );
2015 assert_eq!(
2016 ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
2017 "Vec<f64>"
2018 );
2019 }
2020
2021 #[test]
2022 fn test_nullable_array_elements() {
2023 assert_eq!(
2024 ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
2025 "&[Option<String>]"
2026 );
2027 assert_eq!(
2028 ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
2029 "Vec<Option<String>>"
2030 );
2031 assert_eq!(
2032 ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
2033 "&[Option<f64>]"
2034 );
2035 assert_eq!(
2036 ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
2037 "Vec<Option<f64>>"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_nullable_array_itself() {
2043 assert_eq!(
2044 ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
2045 "Option<&[String]>"
2046 );
2047 assert_eq!(
2048 ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
2049 "Option<Vec<String>>"
2050 );
2051 assert_eq!(
2052 ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
2053 "Option<&[f64]>"
2054 );
2055 assert_eq!(
2056 ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
2057 "Option<Vec<f64>>"
2058 );
2059 }
2060
2061 #[test]
2062 fn test_nullable_array_and_elements() {
2063 assert_eq!(
2064 ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
2065 "Option<&[Option<String>]>"
2066 );
2067 assert_eq!(
2068 ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
2069 "Option<Vec<Option<String>>>"
2070 );
2071 }
2072
2073 #[test]
2074 fn test_fallback_for_union() {
2075 assert_eq!(
2076 ts_type_to_rust_type(Some("string | number"), true).to_string(),
2077 "impl dioxus_use_js::SerdeSerialize"
2078 );
2079 assert_eq!(
2080 ts_type_to_rust_type(Some("string | number"), false).to_string(),
2081 "DeserializeOwned"
2082 );
2083 assert_eq!(
2084 ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
2085 "impl dioxus_use_js::SerdeSerialize"
2086 );
2087 assert_eq!(
2088 ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
2089 "DeserializeOwned"
2090 );
2091 }
2092
2093 #[test]
2094 fn test_unknown_types() {
2095 assert_eq!(
2096 ts_type_to_rust_type(Some("foo"), true).to_string(),
2097 "impl dioxus_use_js::SerdeSerialize"
2098 );
2099 assert_eq!(
2100 ts_type_to_rust_type(Some("foo"), false).to_string(),
2101 "DeserializeOwned"
2102 );
2103
2104 assert_eq!(
2105 ts_type_to_rust_type(Some("any"), true).to_string(),
2106 "impl dioxus_use_js::SerdeSerialize"
2107 );
2108 assert_eq!(
2109 ts_type_to_rust_type(Some("any"), false).to_string(),
2110 "DeserializeOwned"
2111 );
2112 assert_eq!(
2113 ts_type_to_rust_type(Some("object"), true).to_string(),
2114 "impl dioxus_use_js::SerdeSerialize"
2115 );
2116 assert_eq!(
2117 ts_type_to_rust_type(Some("object"), false).to_string(),
2118 "DeserializeOwned"
2119 );
2120 assert_eq!(
2121 ts_type_to_rust_type(Some("unknown"), true).to_string(),
2122 "impl dioxus_use_js::SerdeSerialize"
2123 );
2124 assert_eq!(
2125 ts_type_to_rust_type(Some("unknown"), false).to_string(),
2126 "DeserializeOwned"
2127 );
2128
2129 assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
2130 assert_eq!(
2131 ts_type_to_rust_type(Some("undefined"), false).to_string(),
2132 "()"
2133 );
2134 assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
2135 }
2136
2137 #[test]
2138 fn test_extra_whitespace() {
2139 assert_eq!(
2140 ts_type_to_rust_type(Some(" string | null "), true).to_string(),
2141 "Option<&str>"
2142 );
2143 assert_eq!(
2144 ts_type_to_rust_type(Some(" string | null "), false).to_string(),
2145 "Option<String>"
2146 );
2147 assert_eq!(
2148 ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
2149 "&[String]"
2150 );
2151 assert_eq!(
2152 ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
2153 "Vec<String>"
2154 );
2155 }
2156
2157 #[test]
2158 fn test_map_types() {
2159 assert_eq!(
2160 ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
2161 "&std::collections::HashMap<String, f64>"
2162 );
2163 assert_eq!(
2164 ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
2165 "std::collections::HashMap<String, f64>"
2166 );
2167 assert_eq!(
2168 ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
2169 "&std::collections::HashMap<String, bool>"
2170 );
2171 assert_eq!(
2172 ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
2173 "std::collections::HashMap<String, bool>"
2174 );
2175 assert_eq!(
2176 ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
2177 "&std::collections::HashMap<f64, String>"
2178 );
2179 assert_eq!(
2180 ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
2181 "std::collections::HashMap<f64, String>"
2182 );
2183 }
2184
2185 #[test]
2186 fn test_set_types() {
2187 assert_eq!(
2188 ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
2189 "&std::collections::HashSet<String>"
2190 );
2191 assert_eq!(
2192 ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
2193 "std::collections::HashSet<String>"
2194 );
2195 assert_eq!(
2196 ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
2197 "&std::collections::HashSet<f64>"
2198 );
2199 assert_eq!(
2200 ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
2201 "std::collections::HashSet<f64>"
2202 );
2203 assert_eq!(
2204 ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
2205 "&std::collections::HashSet<bool>"
2206 );
2207 assert_eq!(
2208 ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
2209 "std::collections::HashSet<bool>"
2210 );
2211 }
2212
2213 #[test]
2214 fn test_rust_callback() {
2215 assert_eq!(
2216 ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
2217 "dioxus::core::Callback<f64, impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2218 );
2219 assert_eq!(
2220 ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
2221 "dioxus::core::Callback<(), impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2222 );
2223 assert_eq!(
2224 ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
2225 "dioxus::core::Callback<(), impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2226 );
2227 assert_eq!(
2228 ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
2229 "dioxus::core::Callback<f64, impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2230 );
2231 }
2232
2233 #[test]
2234 fn test_promise_types() {
2235 assert_eq!(
2236 ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
2237 "String"
2238 );
2239 assert_eq!(
2240 ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
2241 "f64"
2242 );
2243 assert_eq!(
2244 ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
2245 "bool"
2246 );
2247 }
2248
2249 #[test]
2250 fn test_json_types() {
2251 assert_eq!(
2252 ts_type_to_rust_type(Some("Json"), true).to_string(),
2253 "&dioxus_use_js::SerdeJsonValue"
2254 );
2255 assert_eq!(
2256 ts_type_to_rust_type(Some("Json"), false).to_string(),
2257 "dioxus_use_js::SerdeJsonValue"
2258 );
2259 }
2260
2261 #[test]
2262 fn test_js_value() {
2263 assert_eq!(
2264 ts_type_to_rust_type(Some("JsValue"), true).to_string(),
2265 "&dioxus_use_js::JsValue"
2266 );
2267 assert_eq!(
2268 ts_type_to_rust_type(Some("JsValue"), false).to_string(),
2269 "dioxus_use_js::JsValue"
2270 );
2271 assert_eq!(
2272 ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
2273 "&dioxus_use_js::JsValue"
2274 );
2275 assert_eq!(
2276 ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
2277 "dioxus_use_js::JsValue"
2278 );
2279
2280 assert_eq!(
2281 ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
2282 "dioxus_use_js::JsValue"
2283 );
2284
2285 assert_eq!(
2286 ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
2287 "Option<dioxus_use_js::JsValue>"
2288 );
2289 assert_eq!(
2290 ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
2291 "Option<&dioxus_use_js::JsValue>"
2292 );
2293 assert_eq!(
2294 ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
2295 "Option<dioxus_use_js::JsValue>"
2296 );
2297 }
2298
2299 #[test]
2300 fn test_class_parsing() {
2301 let ts_content = r#"
2302 /**
2303 * A test class
2304 */
2305 export class MyClass {
2306 constructor(name: string, value: number) {}
2307
2308 /**
2309 * Instance method
2310 */
2311 greet(greeting: string): string {
2312 return greeting;
2313 }
2314
2315 /**
2316 * Async method
2317 */
2318 async fetchData(url: string): Promise<string> {
2319 return "data";
2320 }
2321
2322 /**
2323 * Static method
2324 */
2325 static create(): MyClass {
2326 return new MyClass("test", 0);
2327 }
2328 }
2329 "#;
2330
2331 let source_map = SourceMap::default();
2332 let fm = source_map.new_source_file(
2333 swc_common::FileName::Custom("test.ts".to_string()).into(),
2334 ts_content.to_string(),
2335 );
2336 let comments = SingleThreadedComments::default();
2337
2338 let syntax = Syntax::Typescript(swc_ecma_parser::TsSyntax {
2339 tsx: false,
2340 decorators: false,
2341 dts: false,
2342 no_early_errors: false,
2343 disallow_ambiguous_jsx_like: true,
2344 });
2345
2346 let lexer = Lexer::new(
2347 syntax,
2348 Default::default(),
2349 StringInput::from(&*fm),
2350 Some(&comments),
2351 );
2352
2353 let mut parser = Parser::new_from(lexer);
2354 let module = parser.parse_module().unwrap();
2355
2356 let mut visitor = JsVisitor::new(comments, source_map);
2357 module.visit_with(&mut visitor);
2358
2359 visitor
2361 .classes
2362 .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
2363
2364 assert_eq!(visitor.classes.len(), 1);
2366 let class = &visitor.classes[0];
2367 assert_eq!(class.name, "MyClass");
2368 assert_eq!(class.is_exported, true);
2369
2370 assert_eq!(class.methods.len(), 3);
2372
2373 let greet = &class.methods[0];
2374 assert_eq!(greet.name, "greet");
2375 assert_eq!(greet.is_async, false);
2376 assert_eq!(greet.is_static, false);
2377 assert_eq!(greet.params.len(), 1);
2378 assert_eq!(greet.params[0].name, "greeting");
2379 assert_eq!(greet.params[0].rust_type.to_string(), "&str");
2380 assert_eq!(greet.rust_return_type.to_string(), "String");
2381
2382 let fetch_data = &class.methods[1];
2383 assert_eq!(fetch_data.name, "fetchData");
2384 assert_eq!(fetch_data.is_async, true);
2385 assert_eq!(fetch_data.is_static, false);
2386 assert_eq!(fetch_data.params.len(), 1);
2387 assert_eq!(fetch_data.rust_return_type.to_string(), "String");
2388
2389 let create = &class.methods[2];
2390 assert_eq!(create.name, "create");
2391 assert_eq!(create.is_async, false);
2392 assert_eq!(create.is_static, true);
2393 assert_eq!(create.params.len(), 0);
2394 }
2396}