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