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