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