dioxus_use_js_macro/
lib.rs

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