dioxus_use_js_macro/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use core::panic;
4use proc_macro::TokenStream;
5use proc_macro2::TokenStream as TokenStream2;
6use quote::{format_ident, quote};
7use std::collections::HashMap;
8use std::{fs, path::Path};
9use swc_common::comments::{CommentKind, Comments};
10use swc_common::{SourceMap, Span, comments::SingleThreadedComments};
11use swc_common::{SourceMapper, Spanned};
12use swc_ecma_ast::{
13    Decl, ExportDecl, ExportSpecifier, FnDecl, ModuleExportName, NamedExport, Pat, TsType,
14    TsTypeAnn, VarDeclarator,
15};
16use swc_ecma_parser::EsSyntax;
17use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
18use swc_ecma_visit::{Visit, VisitWith};
19use syn::{
20    Ident, LitStr, Result, Token,
21    parse::{Parse, ParseStream},
22    parse_macro_input,
23};
24
25/// `JsValue<T>`
26const JSVALUE_START: &str = "JsValue";
27const JSVALUE: &str = "dioxus_use_js::JsValue";
28const DEFAULT_INPUT: &str = "impl dioxus_use_js::SerdeSerialize";
29const DEFAULT_OUTPUT: &str = "T: dioxus_use_js::SerdeDeDeserializeOwned";
30const SERDE_VALUE_INPUT: &str = "&dioxus_use_js::SerdeJsonValue";
31const SERDE_VALUE_OUTPUT: &str = "dioxus_use_js::SerdeJsonValue";
32const JSON: &str = "Json";
33/// `RustCallback<T,TT>`
34const RUST_CALLBACK_JS_START: &str = "RustCallback";
35
36#[derive(Debug, Clone)]
37enum ImportSpec {
38    /// *
39    All,
40    /// {greeting, other_func}
41    Named(Vec<Ident>),
42    /// greeting
43    Single(Ident),
44}
45
46struct UseJsInput {
47    js_bundle_path: LitStr,
48    ts_source_path: Option<LitStr>,
49    import_spec: ImportSpec,
50}
51
52impl Parse for UseJsInput {
53    fn parse(input: ParseStream) -> Result<Self> {
54        let first_str: LitStr = input.parse()?;
55
56        // Check if => follows (i.e., we have "src.ts" => "bundle.js")
57        let (ts_source_path, js_bundle_path) = if input.peek(Token![,]) {
58            input.parse::<Token![,]>()?;
59            let second_str: LitStr = input.parse()?;
60            (Some(first_str), second_str)
61        } else {
62            (None, first_str)
63        };
64
65        // Check for optional :: following bundle path
66        let import_spec = if input.peek(Token![::]) {
67            input.parse::<Token![::]>()?;
68
69            if input.peek(Token![*]) {
70                input.parse::<Token![*]>()?;
71                ImportSpec::All
72            } else if input.peek(Ident) {
73                let ident: Ident = input.parse()?;
74                ImportSpec::Single(ident)
75            } else if input.peek(syn::token::Brace) {
76                let content;
77                syn::braced!(content in input);
78                let idents: syn::punctuated::Punctuated<Ident, Token![,]> =
79                    content.parse_terminated(Ident::parse, Token![,])?;
80                ImportSpec::Named(idents.into_iter().collect())
81            } else {
82                return Err(input.error("Expected `*`, an identifier, or a brace group after `::`"));
83            }
84        } else {
85            return Err(input
86                .error("Expected `::` followed by an import spec (even for wildcard with `*`)"));
87        };
88
89        Ok(UseJsInput {
90            js_bundle_path,
91            ts_source_path,
92            import_spec,
93        })
94    }
95}
96
97#[derive(Debug, Clone)]
98struct ParamInfo {
99    name: String,
100    #[allow(unused)]
101    js_type: Option<String>,
102    rust_type: RustType,
103}
104
105#[derive(Debug, Clone)]
106struct FunctionInfo {
107    name: String,
108    /// If specified in the use declaration
109    name_ident: Option<Ident>,
110    /// js param types
111    params: Vec<ParamInfo>,
112    // js return type
113    #[allow(unused)]
114    js_return_type: Option<String>,
115    rust_return_type: RustType,
116    is_exported: bool,
117    is_async: bool,
118    /// The stripped lines
119    doc_comment: Vec<String>,
120}
121
122struct FunctionVisitor {
123    functions: Vec<FunctionInfo>,
124    comments: SingleThreadedComments,
125    source_map: SourceMap,
126}
127
128impl FunctionVisitor {
129    fn new(comments: SingleThreadedComments, source_map: SourceMap) -> Self {
130        Self {
131            functions: Vec::new(),
132            comments,
133            source_map,
134        }
135    }
136
137    fn extract_doc_comment(&self, span: &Span) -> Vec<String> {
138        // Get leading comments for the span
139        let leading_comment = self.comments.get_leading(span.lo());
140
141        if let Some(comments) = leading_comment {
142            let mut doc_lines = Vec::new();
143
144            for comment in comments.iter() {
145                let comment_text = &comment.text;
146                match comment.kind {
147                    // Handle `///`. `//` is already stripped
148                    CommentKind::Line => {
149                        if let Some(content) = comment_text.strip_prefix("/") {
150                            let cleaned = content.trim_start();
151                            doc_lines.push(cleaned.to_string());
152                        }
153                    }
154                    // Handle `/*` `*/`. `/*` `*/` is already stripped
155                    CommentKind::Block => {
156                        for line in comment_text.lines() {
157                            if let Some(cleaned) = line.trim_start().strip_prefix("*") {
158                                doc_lines.push(cleaned.to_string());
159                            }
160                        }
161                    }
162                };
163            }
164
165            doc_lines
166        } else {
167            Vec::new()
168        }
169    }
170}
171
172#[derive(Debug, Clone)]
173enum RustType {
174    Regular(String),
175    CallBack(RustCallback),
176    JsValue(JsValue),
177}
178
179impl ToString for RustType {
180    fn to_string(&self) -> String {
181        match self {
182            RustType::Regular(ty) => ty.clone(),
183            RustType::CallBack(callback) => callback.to_string(),
184            RustType::JsValue(js_value) => js_value.to_string(),
185        }
186    }
187}
188
189impl RustType {
190    fn to_tokens(&self) -> TokenStream2 {
191        self.to_string()
192            .parse::<TokenStream2>()
193            .expect("Calculated Rust type should always be valid")
194    }
195}
196
197#[derive(Debug, Clone)]
198struct RustCallback {
199    input: Option<String>,
200    output: Option<String>,
201}
202
203impl ToString for RustCallback {
204    fn to_string(&self) -> String {
205        let input = self.input.as_deref();
206        let output = self.output.as_deref().unwrap_or("()");
207        format!(
208            "impl AsyncFnMut({}) -> Result<{}, Box<dyn std::error::Error + Send + Sync>>",
209            input.unwrap_or_default(),
210            output
211        )
212    }
213}
214
215#[derive(Debug, Clone)]
216struct JsValue {
217    is_option: bool,
218    is_input: bool,
219}
220
221impl ToString for JsValue {
222    fn to_string(&self) -> String {
223        if self.is_option {
224            format!(
225                "Option<{}>",
226                if self.is_input {
227                    format!("&{}", JSVALUE)
228                } else {
229                    JSVALUE.to_owned()
230                }
231            )
232        } else {
233            if self.is_input {
234                format!("&{}", JSVALUE)
235            } else {
236                JSVALUE.to_owned()
237            }
238        }
239    }
240}
241
242fn strip_parenthesis(mut ts_type: &str) -> &str {
243    while ts_type.starts_with("(") && ts_type.ends_with(")") {
244        ts_type = &ts_type[1..ts_type.len() - 1].trim();
245    }
246    return ts_type;
247}
248
249fn ts_type_to_rust_type(ts_type: Option<&str>, is_input: bool) -> RustType {
250    let Some(mut ts_type) = ts_type else {
251        return RustType::Regular(
252            (if is_input {
253                DEFAULT_INPUT
254            } else {
255                DEFAULT_OUTPUT
256            })
257            .to_owned(),
258        );
259    };
260    ts_type = strip_parenthesis(&mut ts_type);
261    if ts_type.starts_with("Promise<") && ts_type.ends_with(">") {
262        assert!(!is_input, "Promise cannot be used as input type");
263        ts_type = &ts_type[8..ts_type.len() - 1];
264    }
265    ts_type = strip_parenthesis(&mut ts_type);
266    if ts_type.contains(JSVALUE_START) {
267        let parts = split_top_level_union(ts_type);
268        let len = parts.len();
269        if len == 1 && parts[0].starts_with(JSVALUE_START) {
270            return RustType::JsValue(JsValue {
271                is_option: false,
272                is_input,
273            });
274        }
275
276        if len == 2 && parts.contains(&"null") {
277            return RustType::JsValue(JsValue {
278                is_option: true,
279                is_input,
280            });
281        } else {
282            panic!("Invalid use of `{}` for `{}`", JSVALUE_START, ts_type);
283        }
284    }
285    if ts_type.contains(RUST_CALLBACK_JS_START) {
286        if ts_type.starts_with(RUST_CALLBACK_JS_START) {
287            assert!(is_input, "Cannot return a RustCallback: {}", ts_type);
288            let ts_type = &ts_type[RUST_CALLBACK_JS_START.len()..];
289            if ts_type.starts_with("<") && ts_type.ends_with(">") {
290                let inner = &ts_type[1..ts_type.len() - 1];
291                let parts = inner.split(",").collect::<Vec<&str>>();
292                let len = parts.len();
293                if len != 2 {
294                    panic!("A RustCallback type expects two parameters, got: {}", inner);
295                }
296                let input = parts[0].trim();
297                let input = if input == "void" {
298                    None
299                } else {
300                    // `is_input` is false since the deserialized value is only passed to the callback so no point not to give ownership
301                    ts_type_to_rust_type_helper(parts[0].trim(), false, true)
302                };
303                // `RustCallback<T>` or `RustCallback<T,TT>`
304                let output = parts[1].trim();
305                let output = if output == "void" {
306                    None
307                } else {
308                    ts_type_to_rust_type_helper(parts[1].trim(), false, true)
309                };
310                return RustType::CallBack(RustCallback { input, output });
311            } else {
312                panic!("Invalid RustCallback type: {}", ts_type);
313            }
314        } else {
315            panic!("Nested RustCallback is not valid: {}", ts_type);
316        }
317    }
318    RustType::Regular(match ts_type_to_rust_type_helper(ts_type, is_input, true) {
319        Some(value) => value,
320        None => (if is_input {
321            DEFAULT_INPUT
322        } else {
323            DEFAULT_OUTPUT
324        })
325        .to_owned(),
326    })
327}
328
329/// Simple converter, needs null in the second position to enable Option, handles regular ts types.
330/// Does not handle all edge cases
331fn ts_type_to_rust_type_helper(mut ts_type: &str, is_input: bool, is_root: bool) -> Option<String> {
332    ts_type = ts_type.trim();
333    ts_type = strip_parenthesis(&mut ts_type);
334
335    let parts = split_top_level_union(ts_type);
336    if parts.len() > 1 {
337        // Handle single null union: T | null or null | T
338        if parts.len() == 2 && parts.contains(&"null") {
339            let inner = parts.iter().find(|p| **p != "null")?;
340            let inner_rust = ts_type_to_rust_type_helper(inner, is_input, is_root)?;
341            return Some(format!("Option<{}>", inner_rust));
342        }
343        // Unsupported union type
344        return None;
345    }
346
347    ts_type = parts[0];
348
349    if ts_type.ends_with("[]") {
350        let inner = ts_type.strip_suffix("[]").unwrap();
351        let inner_rust = ts_type_to_rust_type_helper(inner, is_input, false)?;
352        return Some(if is_input && is_root {
353            format!("&[{}]", inner_rust)
354        } else {
355            format!("Vec<{}>", inner_rust)
356        });
357    }
358
359    if ts_type.starts_with("Array<") && ts_type.ends_with(">") {
360        let inner = &ts_type[6..ts_type.len() - 1];
361        let inner_rust = ts_type_to_rust_type_helper(inner, is_input, false)?;
362        return Some(if is_input && is_root {
363            format!("&[{}]", inner_rust)
364        } else {
365            format!("Vec<{}>", inner_rust)
366        });
367    }
368
369    if ts_type.starts_with("Set<") && ts_type.ends_with(">") {
370        let inner = &ts_type[4..ts_type.len() - 1];
371        let inner_rust = ts_type_to_rust_type_helper(inner, is_input, false)?;
372        if is_input && is_root {
373            return Some(format!("&std::collections::HashSet<{}>", inner_rust));
374        } else {
375            return Some(format!("std::collections::HashSet<{}>", inner_rust));
376        }
377    }
378
379    if ts_type.starts_with("Map<") && ts_type.ends_with(">") {
380        let inner = &ts_type[4..ts_type.len() - 1];
381        let mut depth = 0;
382        let mut split_index = None;
383        for (i, c) in inner.char_indices() {
384            match c {
385                '<' => depth += 1,
386                '>' => depth -= 1,
387                ',' if depth == 0 => {
388                    split_index = Some(i);
389                    break;
390                }
391                _ => {}
392            }
393        }
394
395        if let Some(i) = split_index {
396            let (key, value) = inner.split_at(i);
397            let value = &value[1..]; // skip comma
398            let key_rust = ts_type_to_rust_type_helper(key.trim(), is_input, false)?;
399            let value_rust = ts_type_to_rust_type_helper(value.trim(), is_input, false)?;
400            if is_input && is_root {
401                return Some(format!(
402                    "&std::collections::HashMap<{}, {}>",
403                    key_rust, value_rust
404                ));
405            } else {
406                return Some(format!(
407                    "std::collections::HashMap<{}, {}>",
408                    key_rust, value_rust
409                ));
410            }
411        } else {
412            return None;
413        }
414    }
415
416    // Base types
417    let rust_type = match ts_type {
418        "string" => {
419            if is_input && is_root {
420                "&str"
421            } else {
422                "String"
423            }
424        }
425        "number" => "f64",
426        "boolean" => "bool",
427        "void" | "undefined" | "never" | "null" => {
428            if is_input {
429                panic!("`{}` is only valid as an output type", ts_type.to_owned());
430            } else if is_root {
431                "()"
432            } else {
433                // Would cause serialization errors since `serde_json::Value::Null` cannot be deserialized into `()`
434                panic!("`{}` is not valid as nested output type", ts_type);
435            }
436        }
437        JSON => {
438            if is_input && is_root {
439                SERDE_VALUE_INPUT
440            } else {
441                SERDE_VALUE_OUTPUT
442            }
443        }
444        "Promise" => {
445            panic!("`{}` - nested promises are not valid", ts_type)
446        }
447        // "any" | "unknown" | "object" | .. etc.
448        _ => {
449            if is_input {
450                DEFAULT_INPUT
451            } else {
452                DEFAULT_OUTPUT
453            }
454        }
455    };
456
457    Some(rust_type.to_owned())
458}
459
460/// Splits e.g. `number | null | string` ignoring nesting like `(number | null)[]`
461fn split_top_level_union(s: &str) -> Vec<&str> {
462    let mut parts = vec![];
463    let mut last = 0;
464    let mut depth_angle = 0;
465    let mut depth_paren = 0;
466
467    for (i, c) in s.char_indices() {
468        match c {
469            '<' => depth_angle += 1,
470            '>' => {
471                if depth_angle > 0 {
472                    depth_angle -= 1
473                }
474            }
475            '(' => depth_paren += 1,
476            ')' => {
477                if depth_paren > 0 {
478                    depth_paren -= 1
479                }
480            }
481            '|' if depth_angle == 0 && depth_paren == 0 => {
482                parts.push(s[last..i].trim());
483                last = i + 1;
484            }
485            _ => {}
486        }
487    }
488
489    if last < s.len() {
490        parts.push(s[last..].trim());
491    }
492
493    parts
494}
495
496fn type_to_string(ty: &Box<TsType>, source_map: &SourceMap) -> String {
497    let span = ty.span();
498    source_map
499        .span_to_snippet(span)
500        .expect("Could not get snippet from span for type")
501}
502
503fn function_pat_to_param_info<'a, I>(pats: I, source_map: &SourceMap) -> Vec<ParamInfo>
504where
505    I: Iterator<Item = &'a Pat>,
506{
507    pats.enumerate()
508        .map(|(i, pat)| to_param_info_helper(i, pat, source_map))
509        .collect()
510}
511
512fn to_param_info_helper(i: usize, pat: &Pat, source_map: &SourceMap) -> ParamInfo {
513    let name = if let Some(ident) = pat.as_ident() {
514        ident.id.sym.to_string()
515    } else {
516        format!("arg{}", i)
517    };
518
519    let js_type = pat
520        .as_ident()
521        .and_then(|ident| ident.type_ann.as_ref())
522        .map(|type_ann| {
523            let ty = &type_ann.type_ann;
524            type_to_string(ty, source_map)
525        });
526    let rust_type = ts_type_to_rust_type(js_type.as_deref(), true);
527
528    ParamInfo {
529        name,
530        js_type,
531        rust_type,
532    }
533}
534
535fn function_info_helper<'a, I>(
536    visitor: &FunctionVisitor,
537    name: String,
538    span: &Span,
539    params: I,
540    return_type: Option<&Box<TsTypeAnn>>,
541    is_async: bool,
542    is_exported: bool,
543) -> FunctionInfo
544where
545    I: Iterator<Item = &'a Pat>,
546{
547    let doc_comment = visitor.extract_doc_comment(span);
548
549    let params = function_pat_to_param_info(params, &visitor.source_map);
550
551    let js_return_type = return_type.as_ref().map(|type_ann| {
552        let ty = &type_ann.type_ann;
553        type_to_string(ty, &visitor.source_map)
554    });
555    if !is_async
556        && let Some(ref js_return_type) = js_return_type
557        && js_return_type.starts_with("Promise")
558    {
559        panic!(
560            "Promise return type is only supported for async functions, use `async fn` instead. For `{js_return_type}`"
561        );
562    }
563
564    let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
565
566    FunctionInfo {
567        name,
568        name_ident: None,
569        params,
570        js_return_type,
571        rust_return_type,
572        is_exported,
573        is_async,
574        doc_comment,
575    }
576}
577
578impl Visit for FunctionVisitor {
579    /// Visit function declarations: function foo() {}
580    fn visit_fn_decl(&mut self, node: &FnDecl) {
581        let name = node.ident.sym.to_string();
582        self.functions.push(function_info_helper(
583            self,
584            name,
585            &node.span(),
586            node.function.params.iter().map(|e| &e.pat),
587            node.function.return_type.as_ref(),
588            node.function.is_async,
589            false,
590        ));
591        node.visit_children_with(self);
592    }
593
594    /// Visit function expressions: const foo = function() {}
595    fn visit_var_declarator(&mut self, node: &VarDeclarator) {
596        if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
597            if let Some(init) = &node.init {
598                let span = node.span();
599                let name = ident.id.sym.to_string();
600                match &**init {
601                    swc_ecma_ast::Expr::Fn(fn_expr) => {
602                        self.functions.push(function_info_helper(
603                            &self,
604                            name,
605                            &span,
606                            fn_expr.function.params.iter().map(|e| &e.pat),
607                            fn_expr.function.return_type.as_ref(),
608                            fn_expr.function.is_async,
609                            false,
610                        ));
611                    }
612                    swc_ecma_ast::Expr::Arrow(arrow_fn) => {
613                        self.functions.push(function_info_helper(
614                            &self,
615                            name,
616                            &span,
617                            arrow_fn.params.iter(),
618                            arrow_fn.return_type.as_ref(),
619                            arrow_fn.is_async,
620                            false,
621                        ));
622                    }
623                    _ => {}
624                }
625            }
626        }
627        node.visit_children_with(self);
628    }
629
630    /// Visit export declarations: export function foo() {}
631    fn visit_export_decl(&mut self, node: &ExportDecl) {
632        if let Decl::Fn(fn_decl) = &node.decl {
633            let span = node.span();
634            let name = fn_decl.ident.sym.to_string();
635            self.functions.push(function_info_helper(
636                &self,
637                name,
638                &span,
639                fn_decl.function.params.iter().map(|e| &e.pat),
640                fn_decl.function.return_type.as_ref(),
641                fn_decl.function.is_async,
642                true,
643            ));
644        }
645        node.visit_children_with(self);
646    }
647
648    /// Visit named exports: export { foo }
649    fn visit_named_export(&mut self, node: &NamedExport) {
650        for spec in &node.specifiers {
651            if let ExportSpecifier::Named(named) = spec {
652                let name = match &named.orig {
653                    ModuleExportName::Ident(ident) => ident.sym.to_string(),
654                    ModuleExportName::Str(str_lit) => str_lit.value.to_string(),
655                };
656
657                if let Some(func) = self.functions.iter_mut().find(|f| f.name == name) {
658                    func.is_exported = true;
659                }
660            }
661        }
662        node.visit_children_with(self);
663    }
664}
665
666fn parse_script_file(file_path: &Path, is_js: bool) -> Result<Vec<FunctionInfo>> {
667    let js_content = fs::read_to_string(file_path).map_err(|e| {
668        syn::Error::new(
669            proc_macro2::Span::call_site(),
670            format!("Could not read file '{}': {}", file_path.display(), e),
671        )
672    })?;
673
674    let source_map = SourceMap::default();
675    let fm = source_map.new_source_file(
676        swc_common::FileName::Custom(file_path.display().to_string()).into(),
677        js_content.clone(),
678    );
679    let comments = SingleThreadedComments::default();
680
681    // Enable TypeScript parsing to handle type annotations
682    let syntax = if is_js {
683        Syntax::Es(EsSyntax {
684            jsx: false,
685            fn_bind: false,
686            decorators: false,
687            decorators_before_export: false,
688            export_default_from: false,
689            import_attributes: false,
690            allow_super_outside_method: false,
691            allow_return_outside_function: false,
692            auto_accessors: false,
693            explicit_resource_management: false,
694        })
695    } else {
696        Syntax::Typescript(swc_ecma_parser::TsSyntax {
697            tsx: false,
698            decorators: false,
699            dts: false,
700            no_early_errors: false,
701            disallow_ambiguous_jsx_like: true,
702        })
703    };
704
705    let lexer = Lexer::new(
706        syntax,
707        Default::default(),
708        StringInput::from(&*fm),
709        Some(&comments),
710    );
711
712    let mut parser = Parser::new_from(lexer);
713
714    let module = parser.parse_module().map_err(|e| {
715        syn::Error::new(
716            proc_macro2::Span::call_site(),
717            format!(
718                "Failed to parse script file '{}': {:?}",
719                file_path.display(),
720                e
721            ),
722        )
723    })?;
724
725    let mut visitor = FunctionVisitor::new(comments, source_map);
726    module.visit_with(&mut visitor);
727
728    // Functions are added twice for some reason
729    visitor
730        .functions
731        .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
732    Ok(visitor.functions)
733}
734
735fn remove_valid_function_info(
736    name: &str,
737    functions: &mut Vec<FunctionInfo>,
738    file: &Path,
739) -> Result<FunctionInfo> {
740    let function_info = if let Some(pos) = functions.iter().position(|f| f.name == name) {
741        functions.remove(pos)
742    } else {
743        return Err(syn::Error::new(
744            proc_macro2::Span::call_site(),
745            format!("Function '{}' not found in file '{}'", name, file.display()),
746        ));
747    };
748    if !function_info.is_exported {
749        return Err(syn::Error::new(
750            proc_macro2::Span::call_site(),
751            format!(
752                "Function '{}' not exported in file '{}'",
753                name,
754                file.display()
755            ),
756        ));
757    }
758    Ok(function_info)
759}
760
761fn get_functions_to_generate(
762    mut functions: Vec<FunctionInfo>,
763    import_spec: &ImportSpec,
764    file: &Path,
765) -> Result<Vec<FunctionInfo>> {
766    match import_spec {
767        ImportSpec::All => Ok(functions),
768        ImportSpec::Single(name) => {
769            let mut func =
770                remove_valid_function_info(name.to_string().as_str(), &mut functions, file)?;
771            func.name_ident.replace(name.clone());
772            Ok(vec![func])
773        }
774        ImportSpec::Named(names) => {
775            let mut result = Vec::new();
776            for name in names {
777                let mut func =
778                    remove_valid_function_info(name.to_string().as_str(), &mut functions, file)?;
779                func.name_ident.replace(name.clone());
780                result.push(func);
781            }
782            Ok(result)
783        }
784    }
785}
786
787fn generate_function_wrapper(func: &FunctionInfo, asset_path: &LitStr) -> TokenStream2 {
788    // If we have callbacks, we cant do a simpl return, we have to do message passing
789    let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
790    let mut callback_name_to_info: HashMap<String, &RustCallback> = HashMap::new();
791    let mut index: u64 = 1; // 0 is the return value
792    for param in &func.params {
793        if let RustType::CallBack(callback) = &param.rust_type {
794            callback_name_to_index.insert(param.name.to_owned(), index);
795            index += 1;
796            callback_name_to_info.insert(param.name.to_owned(), callback);
797        }
798    }
799
800    let send_calls: Vec<TokenStream2> = func
801        .params
802        .iter()
803        .flat_map(|param| {
804            let param_name = format_ident!("{}", param.name);
805            match &param.rust_type {
806                RustType::Regular(_) => Some(quote! {
807                    eval.send(#param_name).map_err(dioxus_use_js::JsError::Eval)?;
808                }),
809                RustType::JsValue(js_value) => {
810                    if js_value.is_option {
811                        Some(quote! {
812                            #[allow(deprecated)]
813                            eval.send(#param_name.map(|e| e.internal_get())).map_err(dioxus_use_js::JsError::Eval)?;
814                        })
815                    } else {
816                        Some(quote! {
817                            #[allow(deprecated)]
818                            eval.send(#param_name.internal_get()).map_err(dioxus_use_js::JsError::Eval)?;
819                        })
820                    }
821                },
822                RustType::CallBack(_) => None,
823            }
824        })
825        .collect();
826
827    let js_func_name = &func.name;
828    let params_list = func
829        .params
830        .iter()
831        .map(|p| p.name.as_str())
832        .collect::<Vec<&str>>()
833        .join(", ");
834    let param_declaration_lines = func
835        .params
836        .iter()
837        .map(|param| match &param.rust_type {
838            RustType::Regular(_) => {
839                format!("let {} = await dioxus.recv();", param.name)
840            }
841            RustType::JsValue(js_value) => {
842                let param_name = &param.name;
843                if js_value.is_option {
844                format!(
845                    "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = null;\nif ({param_name}Temp_ !== null) {{{{ {param_name} = window[{param_name}Temp_] }}}};",
846                )
847            }
848            else {
849                format!(
850                    "let {param_name}Temp_ = await dioxus.recv();\nlet {param_name} = window[{param_name}Temp_];",
851                )
852            }
853            },
854            RustType::CallBack(rust_callback) => {
855                let name = &param.name;
856                let index = callback_name_to_index.get(name).unwrap();
857                let RustCallback { input, output } = rust_callback;
858                match (input, output) {
859                    (None, None) => {
860                        // no return, but still need to await ack
861                        format!(
862                            "const {} = async () => {{{{ dioxus.send([{}, null]); await dioxus.recv(); }}}};",
863                            name, index
864                        )
865                    },
866                    (None, Some(_)) => {
867                        format!(
868                            "const {} = async () => {{{{ dioxus.send([{}, null]); return await dioxus.recv(); }}}};",
869                            name, index
870
871                        )
872                    },
873                    (Some(_), None) => {
874                        // no return, but still need to await ack
875                        format!(
876                            "const {} = async (value) => {{{{ dioxus.send([{}, value]); await dioxus.recv(); }}}};",
877                            name, index
878                        )
879                    },
880                    (Some(_), Some(_)) => {
881                        format!(
882                            "const {} = async (value) => {{{{ dioxus.send([{}, value]); return await dioxus.recv(); }}}};",
883                            name, index
884                        )
885                    },
886                }
887            },
888        })
889        .collect::<Vec<_>>()
890        .join("\n");
891    let mut await_fn = String::new();
892    if func.is_async {
893        await_fn.push_str("await");
894    }
895    let call_function = match &func.rust_return_type {
896        RustType::Regular(_) => {
897            // eval will fail if returning undefined. undefined happens if there is no return type
898            format!(
899                r#"
900___result___ = {await_fn} {js_func_name}({params_list});
901"#
902            )
903        }
904        RustType::CallBack(_) => panic!("Cannot be an output type, should have panicked earlier."),
905        RustType::JsValue(js_value) => {
906            let check = if js_value.is_option {
907                // null or undefined is valid, since this is e.g. `Option<JsValue>`
908                "if (___resultValue___ === null || ___resultValue___ === undefined) {{{{ return null; }}}}".to_owned()
909            } else {
910                format!("if (___resultValue___ === undefined) {{{{ console.error(\"`{js_func_name}` was undefined, but value is needed for JsValue\"); return null; }}}}")
911            };
912            format!(
913                r#"
914const ___resultValue___ = {await_fn} {js_func_name}({params_list});
915{check}
916___result___ = "js-value-{js_func_name}-" + crypto.randomUUID();
917window[___result___] = ___resultValue___;
918        "#
919            )
920        }
921    };
922    let end_statement = if callback_name_to_index.is_empty() {
923        "if (___result___ === undefined) {{ return null; }}; return ___result___;"
924    } else {
925        "if (___result___ === undefined) {{ dioxus.send([0, null]); }}; dioxus.send([0, ___result___]);"
926    };
927
928    let js_format = format!(
929        r#"
930const {{{{ {js_func_name} }}}} = await import("{{}}");
931{param_declaration_lines}
932let ___result___;
933try {{{{
934{call_function}
935}}}}
936catch (e) {{{{
937console.error("Executing function `{js_func_name}` threw an error:", e);
938___result___ = undefined;
939}}}}
940{end_statement}
941"#
942    );
943
944    // Generate parameter types with extracted type information
945    let param_types: Vec<_> = func
946        .params
947        .iter()
948        .map(|param| {
949            let param_name = format_ident!("{}", param.name);
950            let type_tokens = param.rust_type.to_tokens();
951            if let RustType::CallBack(_) = param.rust_type {
952                quote! { mut #param_name: #type_tokens }
953            } else {
954                quote! { #param_name: #type_tokens }
955            }
956        })
957        .collect();
958
959    let parsed_type = func.rust_return_type.to_tokens();
960    let (return_type_tokens, generic_tokens) =
961        if func.rust_return_type.to_string() == DEFAULT_OUTPUT {
962            (
963                quote! { Result<T, dioxus_use_js::JsError> },
964                Some(quote! { <#parsed_type> }),
965            )
966        } else {
967            (
968                quote! { Result<#parsed_type, dioxus_use_js::JsError> },
969                None,
970            )
971        };
972
973    // Generate documentation comment if available - preserve original JSDoc format
974    let doc_comment = if func.doc_comment.is_empty() {
975        quote! {}
976    } else {
977        let doc_lines: Vec<_> = func
978            .doc_comment
979            .iter()
980            .map(|line| quote! { #[doc = #line] })
981            .collect();
982        quote! { #(#doc_lines)* }
983    };
984
985    let func_name = func
986        .name_ident
987        .clone()
988        // Can not exist if `::*`
989        .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
990
991    // void like returns always send back "Null" as an ack
992    let void_output_mapping = if func.rust_return_type.to_string() == "()" {
993        quote! {
994            .and_then(|e| {
995                if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
996                    Ok(())
997                } else {
998                    Err(dioxus_use_js::JsError::Eval(
999                        dioxus::document::EvalError::Serialization(
1000                            <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1001                        )
1002                    ))
1003                }
1004            })
1005        }
1006    } else {
1007        quote! {}
1008    };
1009
1010    let has_no_callbacks = callback_name_to_index.is_empty();
1011    let end_statement = if has_no_callbacks {
1012        let return_value_mapping = if func.rust_return_type.to_string() == SERDE_VALUE_OUTPUT {
1013            quote! {
1014                .map_err(dioxus_use_js::JsError::Eval)
1015            }
1016        } else {
1017            quote! {
1018                .map_err(dioxus_use_js::JsError::Eval)
1019                .and_then(|v| dioxus_use_js::serde_json_from_value(v).map_err(|e| dioxus_use_js::JsError::Eval(dioxus::document::EvalError::Serialization(e))))
1020            }
1021        };
1022
1023        match &func.rust_return_type {
1024            RustType::Regular(_) => {
1025                quote! {
1026                    eval
1027                        .await
1028                        #return_value_mapping
1029                        #void_output_mapping
1030                }
1031            }
1032            RustType::CallBack(_) => {
1033                panic!("Cannot be an output type, should have panicked earlier.")
1034            }
1035            RustType::JsValue(js_value) => {
1036                if js_value.is_option {
1037                    quote! {
1038                    let id: Option<String> = eval
1039                        .await
1040                        #return_value_mapping?;
1041                        #[allow(deprecated)]
1042                        Ok(id.map(|e| dioxus_use_js::JsValue::internal_create(e)))
1043                    }
1044                } else {
1045                    quote! {
1046                    let id: String = eval
1047                        .await
1048                        #return_value_mapping?;
1049                        #[allow(deprecated)]
1050                        Ok(dioxus_use_js::JsValue::internal_create(id))
1051                    }
1052                }
1053            }
1054        }
1055    } else {
1056        let callback_arms: Vec<TokenStream2> = callback_name_to_index
1057            .iter()
1058            .map(|(name, index)| {
1059                let callback = callback_name_to_info.get(name).unwrap();
1060                let callback_call = if let Some(_) = callback.input {
1061                    quote! {
1062                        let value = dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1063                            dioxus_use_js::JsError::Eval(
1064                                dioxus::document::EvalError::Serialization(e),
1065                            )
1066                        })?;
1067                        let value = match callback(value).await {
1068                            Ok(value) => value,
1069                            Err(error) => {
1070                                return Err(dioxus_use_js::JsError::Callback(error));
1071                            }
1072                        };
1073                    }
1074                } else {
1075                    quote! {
1076                        let value = match callback().await {
1077                            Ok(value) => value,
1078                            Err(error) => {
1079                                return Err(dioxus_use_js::JsError::Callback(error));
1080                            }
1081                        };
1082                    }
1083                };
1084
1085                let callback_send_back = if let Some(_) = callback.output {
1086                    quote! {
1087                        eval.send(value).map_err(dioxus_use_js::JsError::Eval)?;
1088                    }
1089                } else {
1090                    // send ack
1091                    quote! {
1092                        eval.send(dioxus_use_js::SerdeJsonValue::Null).map_err(dioxus_use_js::JsError::Eval)?;
1093                    }
1094                };
1095                quote! {
1096                    #index => {
1097                        #callback_call
1098                        #callback_send_back
1099                    }
1100                }
1101            })
1102            .collect();
1103
1104        quote! {
1105        loop {
1106            let value = eval
1107                .recv::<dioxus_use_js::SerdeJsonValue>()
1108                .await
1109                .map_err(dioxus_use_js::JsError::Eval)?;
1110            match value{
1111                dioxus_use_js::SerdeJsonValue::Array(values) => {
1112                    if values.len() != 2 {
1113                        unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG)
1114                    }
1115                    let mut iter = values.into_iter();
1116                    let action_ = match iter.next().unwrap() {
1117                        dioxus_use_js::SerdeJsonValue::Number(action_) => action_,
1118                        _ => unreachable!("{}", dioxus_use_js::__INDEX_VALIDATION_MSG),
1119                    };
1120                    let value = iter.next().unwrap();
1121                    match action_.as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG) {
1122                        0 => {
1123                            return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1124                                dioxus_use_js::JsError::Eval(
1125                                    dioxus::document::EvalError::Serialization(e),
1126                                )
1127                            })
1128                            #void_output_mapping;
1129                        }
1130                        #(#callback_arms,)*
1131                        _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1132                    }
1133                }
1134                _ => unreachable!("{}", dioxus_use_js::__SEND_VALIDATION_MSG),
1135            }
1136        }
1137        }
1138    };
1139
1140    quote! {
1141        #doc_comment
1142        #[allow(non_snake_case)]
1143        pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1144            const MODULE: Asset = asset!(#asset_path);
1145            let js = format!(#js_format, MODULE);
1146            let mut eval = dioxus::document::eval(js.as_str());
1147            #(#send_calls)*
1148            #end_statement
1149        }
1150    }
1151}
1152
1153/// A macro to create rust bindings to javascript and typescript functions. See [README](https://github.com/mcmah309/dioxus-use-js) and [example](https://github.com/mcmah309/dioxus-use-js/blob/master/example/src/main.rs) for more.
1154#[proc_macro]
1155pub fn use_js(input: TokenStream) -> TokenStream {
1156    let input = parse_macro_input!(input as UseJsInput);
1157
1158    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1159        Ok(dir) => dir,
1160        Err(_) => {
1161            return TokenStream::from(
1162                syn::Error::new(
1163                    proc_macro2::Span::call_site(),
1164                    "CARGO_MANIFEST_DIR environment variable not found",
1165                )
1166                .to_compile_error(),
1167            );
1168        }
1169    };
1170
1171    let UseJsInput {
1172        js_bundle_path,
1173        ts_source_path,
1174        import_spec,
1175    } = input;
1176
1177    let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1178
1179    let js_all_functions = match parse_script_file(&js_file_path, true) {
1180        Ok(funcs) => funcs,
1181        Err(e) => return TokenStream::from(e.to_compile_error()),
1182    };
1183
1184    let js_functions_to_generate =
1185        match get_functions_to_generate(js_all_functions, &import_spec, &js_file_path) {
1186            Ok(funcs) => funcs,
1187            Err(e) => return TokenStream::from(e.to_compile_error()),
1188        };
1189
1190    let functions_to_generate = if let Some(ts_file_path) = ts_source_path {
1191        let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1192        let ts_all_functions = match parse_script_file(&ts_file_path, false) {
1193            Ok(funcs) => funcs,
1194            Err(e) => return TokenStream::from(e.to_compile_error()),
1195        };
1196
1197        let ts_functions_to_generate =
1198            match get_functions_to_generate(ts_all_functions, &import_spec, &ts_file_path) {
1199                Ok(funcs) => funcs,
1200                Err(e) => {
1201                    return TokenStream::from(e.to_compile_error());
1202                }
1203            };
1204
1205        for ts_func in ts_functions_to_generate.iter() {
1206            if let Some(js_func) = js_functions_to_generate
1207                .iter()
1208                .find(|f| f.name == ts_func.name)
1209            {
1210                if ts_func.params.len() != js_func.params.len() {
1211                    return TokenStream::from(syn::Error::new(
1212                        proc_macro2::Span::call_site(),
1213                        format!(
1214                            "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1215                            ts_func.name
1216                        ),
1217                    )
1218                    .to_compile_error());
1219                }
1220            } else {
1221                return TokenStream::from(syn::Error::new(
1222                    proc_macro2::Span::call_site(),
1223                    format!(
1224                        "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1225                        ts_func.name
1226                    ),
1227                )
1228                .to_compile_error());
1229            }
1230        }
1231        ts_functions_to_generate
1232    } else {
1233        js_functions_to_generate
1234    };
1235
1236    let function_wrappers: Vec<TokenStream2> = functions_to_generate
1237        .iter()
1238        .map(|func| generate_function_wrapper(func, &js_bundle_path))
1239        .collect();
1240
1241    let expanded = quote! {
1242        #(#function_wrappers)*
1243    };
1244
1245    TokenStream::from(expanded)
1246}
1247
1248//************************************************************************//
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253
1254    #[test]
1255    fn test_primitives() {
1256        assert_eq!(
1257            ts_type_to_rust_type(Some("string"), false).to_string(),
1258            "String"
1259        );
1260        assert_eq!(
1261            ts_type_to_rust_type(Some("string"), true).to_string(),
1262            "&str"
1263        );
1264        assert_eq!(
1265            ts_type_to_rust_type(Some("number"), false).to_string(),
1266            "f64"
1267        );
1268        assert_eq!(
1269            ts_type_to_rust_type(Some("number"), true).to_string(),
1270            "f64"
1271        );
1272        assert_eq!(
1273            ts_type_to_rust_type(Some("boolean"), false).to_string(),
1274            "bool"
1275        );
1276        assert_eq!(
1277            ts_type_to_rust_type(Some("boolean"), true).to_string(),
1278            "bool"
1279        );
1280    }
1281
1282    #[test]
1283    fn test_nullable_primitives() {
1284        assert_eq!(
1285            ts_type_to_rust_type(Some("string | null"), true).to_string(),
1286            "Option<&str>"
1287        );
1288        assert_eq!(
1289            ts_type_to_rust_type(Some("string | null"), false).to_string(),
1290            "Option<String>"
1291        );
1292        assert_eq!(
1293            ts_type_to_rust_type(Some("number | null"), true).to_string(),
1294            "Option<f64>"
1295        );
1296        assert_eq!(
1297            ts_type_to_rust_type(Some("number | null"), false).to_string(),
1298            "Option<f64>"
1299        );
1300        assert_eq!(
1301            ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1302            "Option<bool>"
1303        );
1304        assert_eq!(
1305            ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1306            "Option<bool>"
1307        );
1308    }
1309
1310    #[test]
1311    fn test_arrays() {
1312        assert_eq!(
1313            ts_type_to_rust_type(Some("string[]"), true).to_string(),
1314            "&[String]"
1315        );
1316        assert_eq!(
1317            ts_type_to_rust_type(Some("string[]"), false).to_string(),
1318            "Vec<String>"
1319        );
1320        assert_eq!(
1321            ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
1322            "&[f64]"
1323        );
1324        assert_eq!(
1325            ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
1326            "Vec<f64>"
1327        );
1328    }
1329
1330    #[test]
1331    fn test_nullable_array_elements() {
1332        assert_eq!(
1333            ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
1334            "&[Option<String>]"
1335        );
1336        assert_eq!(
1337            ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
1338            "Vec<Option<String>>"
1339        );
1340        assert_eq!(
1341            ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
1342            "&[Option<f64>]"
1343        );
1344        assert_eq!(
1345            ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
1346            "Vec<Option<f64>>"
1347        );
1348    }
1349
1350    #[test]
1351    fn test_nullable_array_itself() {
1352        assert_eq!(
1353            ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
1354            "Option<&[String]>"
1355        );
1356        assert_eq!(
1357            ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
1358            "Option<Vec<String>>"
1359        );
1360        assert_eq!(
1361            ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
1362            "Option<&[f64]>"
1363        );
1364        assert_eq!(
1365            ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
1366            "Option<Vec<f64>>"
1367        );
1368    }
1369
1370    #[test]
1371    fn test_nullable_array_and_elements() {
1372        assert_eq!(
1373            ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
1374            "Option<&[Option<String>]>"
1375        );
1376        assert_eq!(
1377            ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
1378            "Option<Vec<Option<String>>>"
1379        );
1380    }
1381
1382    #[test]
1383    fn test_fallback_for_union() {
1384        assert_eq!(
1385            ts_type_to_rust_type(Some("string | number"), true).to_string(),
1386            "impl dioxus_use_js::SerdeSerialize"
1387        );
1388        assert_eq!(
1389            ts_type_to_rust_type(Some("string | number"), false).to_string(),
1390            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1391        );
1392        assert_eq!(
1393            ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
1394            "impl dioxus_use_js::SerdeSerialize"
1395        );
1396        assert_eq!(
1397            ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
1398            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1399        );
1400    }
1401
1402    #[test]
1403    fn test_unknown_types() {
1404        assert_eq!(
1405            ts_type_to_rust_type(Some("foo"), true).to_string(),
1406            "impl dioxus_use_js::SerdeSerialize"
1407        );
1408        assert_eq!(
1409            ts_type_to_rust_type(Some("foo"), false).to_string(),
1410            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1411        );
1412
1413        assert_eq!(
1414            ts_type_to_rust_type(Some("any"), true).to_string(),
1415            "impl dioxus_use_js::SerdeSerialize"
1416        );
1417        assert_eq!(
1418            ts_type_to_rust_type(Some("any"), false).to_string(),
1419            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1420        );
1421        assert_eq!(
1422            ts_type_to_rust_type(Some("object"), true).to_string(),
1423            "impl dioxus_use_js::SerdeSerialize"
1424        );
1425        assert_eq!(
1426            ts_type_to_rust_type(Some("object"), false).to_string(),
1427            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1428        );
1429        assert_eq!(
1430            ts_type_to_rust_type(Some("unknown"), true).to_string(),
1431            "impl dioxus_use_js::SerdeSerialize"
1432        );
1433        assert_eq!(
1434            ts_type_to_rust_type(Some("unknown"), false).to_string(),
1435            "T: dioxus_use_js::SerdeDeDeserializeOwned"
1436        );
1437
1438        assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
1439        assert_eq!(
1440            ts_type_to_rust_type(Some("undefined"), false).to_string(),
1441            "()"
1442        );
1443        assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
1444    }
1445
1446    #[test]
1447    fn test_extra_whitespace() {
1448        assert_eq!(
1449            ts_type_to_rust_type(Some("  string | null  "), true).to_string(),
1450            "Option<&str>"
1451        );
1452        assert_eq!(
1453            ts_type_to_rust_type(Some("  string | null  "), false).to_string(),
1454            "Option<String>"
1455        );
1456        assert_eq!(
1457            ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
1458            "&[String]"
1459        );
1460        assert_eq!(
1461            ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
1462            "Vec<String>"
1463        );
1464    }
1465
1466    #[test]
1467    fn test_map_types() {
1468        assert_eq!(
1469            ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
1470            "&std::collections::HashMap<String, f64>"
1471        );
1472        assert_eq!(
1473            ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
1474            "std::collections::HashMap<String, f64>"
1475        );
1476        assert_eq!(
1477            ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
1478            "&std::collections::HashMap<String, bool>"
1479        );
1480        assert_eq!(
1481            ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
1482            "std::collections::HashMap<String, bool>"
1483        );
1484        assert_eq!(
1485            ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
1486            "&std::collections::HashMap<f64, String>"
1487        );
1488        assert_eq!(
1489            ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
1490            "std::collections::HashMap<f64, String>"
1491        );
1492    }
1493
1494    #[test]
1495    fn test_set_types() {
1496        assert_eq!(
1497            ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
1498            "&std::collections::HashSet<String>"
1499        );
1500        assert_eq!(
1501            ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
1502            "std::collections::HashSet<String>"
1503        );
1504        assert_eq!(
1505            ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
1506            "&std::collections::HashSet<f64>"
1507        );
1508        assert_eq!(
1509            ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
1510            "std::collections::HashSet<f64>"
1511        );
1512        assert_eq!(
1513            ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
1514            "&std::collections::HashSet<bool>"
1515        );
1516        assert_eq!(
1517            ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
1518            "std::collections::HashSet<bool>"
1519        );
1520    }
1521
1522    #[test]
1523    fn test_rust_callback() {
1524        assert_eq!(
1525            ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
1526            "impl AsyncFnMut(f64) -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1527        );
1528        assert_eq!(
1529            ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
1530            "impl AsyncFnMut() -> Result<String, Box<dyn std::error::Error + Send + Sync>>"
1531        );
1532        assert_eq!(
1533            ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
1534            "impl AsyncFnMut() -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1535        );
1536        assert_eq!(
1537            ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
1538            "impl AsyncFnMut(f64) -> Result<(), Box<dyn std::error::Error + Send + Sync>>"
1539        );
1540    }
1541
1542    #[test]
1543    fn test_promise_types() {
1544        assert_eq!(
1545            ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
1546            "String"
1547        );
1548        assert_eq!(
1549            ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
1550            "f64"
1551        );
1552        assert_eq!(
1553            ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
1554            "bool"
1555        );
1556    }
1557
1558    #[test]
1559    fn test_json_types() {
1560        assert_eq!(
1561            ts_type_to_rust_type(Some("Json"), true).to_string(),
1562            "&dioxus_use_js::SerdeJsonValue"
1563        );
1564        assert_eq!(
1565            ts_type_to_rust_type(Some("Json"), false).to_string(),
1566            "dioxus_use_js::SerdeJsonValue"
1567        );
1568    }
1569
1570    #[test]
1571    fn test_js_value() {
1572        assert_eq!(
1573            ts_type_to_rust_type(Some("JsValue"), true).to_string(),
1574            "&dioxus_use_js::JsValue"
1575        );
1576        assert_eq!(
1577            ts_type_to_rust_type(Some("JsValue"), false).to_string(),
1578            "dioxus_use_js::JsValue"
1579        );
1580        assert_eq!(
1581            ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
1582            "&dioxus_use_js::JsValue"
1583        );
1584        assert_eq!(
1585            ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
1586            "dioxus_use_js::JsValue"
1587        );
1588
1589        assert_eq!(
1590            ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
1591            "dioxus_use_js::JsValue"
1592        );
1593
1594        assert_eq!(
1595            ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
1596            "Option<dioxus_use_js::JsValue>"
1597        );
1598        assert_eq!(
1599            ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
1600            "Option<&dioxus_use_js::JsValue>"
1601        );
1602        assert_eq!(
1603            ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
1604            "Option<dioxus_use_js::JsValue>"
1605        );
1606    }
1607}