dioxus_use_js_macro/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use base64::Engine;
4use core::panic;
5use indexmap::IndexMap;
6use proc_macro::TokenStream;
7use proc_macro2::{Literal, TokenStream as TokenStream2};
8use quote::{format_ident, quote};
9use std::collections::HashMap;
10use std::str::FromStr;
11use std::{fs, path::Path};
12use swc_common::comments::{CommentKind, Comments};
13use swc_common::{SourceMap, comments::SingleThreadedComments};
14use swc_common::{SourceMapper, Spanned};
15use swc_ecma_ast::{
16    ClassDecl, ClassMember, Decl, ExportDecl, ExportSpecifier, FnDecl, NamedExport, Pat, PropName,
17    TsType, TsTypeAnn, VarDeclarator,
18};
19use swc_ecma_parser::EsSyntax;
20use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
21use swc_ecma_visit::{Visit, VisitWith};
22use syn::TypeParam;
23use syn::{
24    Ident, LitStr, Result, Token,
25    parse::{Parse, ParseStream},
26    parse_macro_input,
27};
28
29/// `JsValue<T>`
30const JSVALUE_START: &str = "JsValue";
31const JSVALUE: &str = "dioxus_use_js::JsValue";
32const DEFAULT_GENRIC_INPUT: &str = "impl dioxus_use_js::SerdeSerialize";
33const DEFAULT_GENERIC_OUTPUT: &str = "DeserializeOwned";
34const DEFAULT_OUTPUT_GENERIC_DECLARTION: &str =
35    "DeserializeOwned: dioxus_use_js::SerdeDeDeserializeOwned";
36const SERDE_VALUE: &str = "dioxus_use_js::SerdeJsonValue";
37const JSON: &str = "Json";
38/// `RustCallback<T,TT>`
39const RUST_CALLBACK_JS_START: &str = "RustCallback";
40const UNIT: &str = "()";
41const DROP_TYPE: &str = "Drop";
42const DROP_NAME: &str = "drop";
43
44#[derive(Debug, Clone)]
45enum ImportSpec {
46    /// *
47    All,
48    /// {greeting, other_func}
49    Named(Vec<Ident>),
50    /// greeting
51    Single(Ident),
52}
53
54struct UseJsInput {
55    js_bundle_path: LitStr,
56    ts_source_path: Option<LitStr>,
57    import_spec: ImportSpec,
58}
59
60impl Parse for UseJsInput {
61    fn parse(input: ParseStream) -> Result<Self> {
62        let first_str: LitStr = input.parse()?;
63
64        // Check if => follows (i.e., we have "src.ts" => "bundle.js")
65        let (ts_source_path, js_bundle_path) = if input.peek(Token![,]) {
66            input.parse::<Token![,]>()?;
67            let second_str: LitStr = input.parse()?;
68            (Some(first_str), second_str)
69        } else {
70            (None, first_str)
71        };
72
73        // Check for optional :: following bundle path
74        let import_spec = if input.peek(Token![::]) {
75            input.parse::<Token![::]>()?;
76
77            if input.peek(Token![*]) {
78                input.parse::<Token![*]>()?;
79                ImportSpec::All
80            } else if input.peek(Ident) {
81                let ident: Ident = input.parse()?;
82                ImportSpec::Single(ident)
83            } else if input.peek(syn::token::Brace) {
84                let content;
85                syn::braced!(content in input);
86                let idents: syn::punctuated::Punctuated<Ident, Token![,]> =
87                    content.parse_terminated(Ident::parse, Token![,])?;
88                ImportSpec::Named(idents.into_iter().collect())
89            } else {
90                return Err(input.error("Expected `*`, an identifier, or a brace group after `::`"));
91            }
92        } else {
93            return Err(input
94                .error("Expected `::` followed by an import spec (even for wildcard with `*`)"));
95        };
96
97        Ok(UseJsInput {
98            js_bundle_path,
99            ts_source_path,
100            import_spec,
101        })
102    }
103}
104
105#[derive(Debug, Clone)]
106struct ParamInfo {
107    name: String,
108    js_type: Option<String>,
109    rust_type: RustType,
110}
111
112impl ParamInfo {
113    fn is_drop(&self) -> bool {
114        match self.js_type.as_ref() {
115            Some(js_type) => js_type == DROP_TYPE,
116            None => self.name == DROP_NAME,
117        }
118    }
119}
120
121#[derive(Debug, Clone)]
122struct FunctionInfo {
123    name: String,
124    /// If specified in the `use_js!` declaration. Used to link the generated code to this span
125    ident: Option<Ident>,
126    /// js param types
127    params: Vec<ParamInfo>,
128    // js return type
129    js_return_type: Option<String>,
130    rust_return_type: RustType,
131    is_exported: bool,
132    is_async: bool,
133    /// The stripped lines
134    doc_comment: Vec<String>,
135}
136
137#[derive(Debug, Clone)]
138struct MethodInfo {
139    name: String,
140    /// js param types
141    params: Vec<ParamInfo>,
142    // js return type
143    js_return_type: Option<String>,
144    rust_return_type: RustType,
145    is_async: bool,
146    is_static: bool,
147    /// The stripped lines
148    doc_comment: Vec<String>,
149}
150
151#[derive(Debug, Clone)]
152#[allow(dead_code)]
153struct ClassInfo {
154    name: String,
155    /// If specified in the `use_js!` declaration. Used to link the generated code to this span
156    ident: Option<Ident>,
157    /// Class methods
158    methods: Vec<MethodInfo>,
159    is_exported: bool,
160    /// The stripped lines
161    doc_comment: Vec<String>,
162}
163
164struct JsVisitor {
165    functions: Vec<FunctionInfo>,
166    classes: Vec<ClassInfo>,
167    comments: SingleThreadedComments,
168    source_map: SourceMap,
169}
170
171impl JsVisitor {
172    fn new(comments: SingleThreadedComments, source_map: SourceMap) -> Self {
173        Self {
174            functions: Vec::new(),
175            classes: Vec::new(),
176            comments,
177            source_map,
178        }
179    }
180
181    fn extract_doc_comment(&self, span: &swc_common::Span) -> Vec<String> {
182        // Get leading comments for the span
183        let leading_comment = self.comments.get_leading(span.lo());
184
185        if let Some(comments) = leading_comment {
186            let mut doc_lines = Vec::new();
187
188            for comment in comments.iter() {
189                let comment_text = &comment.text;
190                match comment.kind {
191                    // Handle `///`. `//` is already stripped
192                    CommentKind::Line => {
193                        if let Some(content) = comment_text.strip_prefix("/") {
194                            let cleaned = content.trim_start();
195                            doc_lines.push(cleaned.to_string());
196                        }
197                    }
198                    // Handle `/*` `*/`. `/*` `*/` is already stripped
199                    CommentKind::Block => {
200                        for line in comment_text.lines() {
201                            if let Some(cleaned) = line.trim_start().strip_prefix("*") {
202                                doc_lines.push(cleaned.to_string());
203                            }
204                        }
205                    }
206                };
207            }
208
209            doc_lines
210        } else {
211            Vec::new()
212        }
213    }
214}
215
216#[derive(Debug, Clone)]
217enum RustType {
218    Regular(String),
219    Callback(RustCallback),
220    JsValue(JsValue),
221}
222
223impl ToString for RustType {
224    fn to_string(&self) -> String {
225        match self {
226            RustType::Regular(ty) => ty.clone(),
227            RustType::Callback(callback) => callback.to_string(),
228            RustType::JsValue(js_value) => js_value.to_string(),
229        }
230    }
231}
232
233impl RustType {
234    fn to_tokens(&self) -> TokenStream2 {
235        self.to_string()
236            .parse::<TokenStream2>()
237            .expect("Calculated Rust type should always be valid")
238    }
239}
240
241#[derive(Debug, Clone)]
242struct RustCallback {
243    input: Option<String>,
244    output: Option<String>,
245}
246
247impl ToString for RustCallback {
248    fn to_string(&self) -> String {
249        let input = self.input.as_deref();
250        let output = self.output.as_deref().unwrap_or(UNIT);
251        format!(
252            "dioxus::core::Callback<{}, impl Future<Output = Result<{}, dioxus_use_js::SerdeJsonValue>> + 'static>",
253            input.unwrap_or("()"),
254            output
255        )
256    }
257}
258
259#[derive(Debug, Clone)]
260struct JsValue {
261    is_option: bool,
262    is_input: bool,
263}
264
265impl ToString for JsValue {
266    fn to_string(&self) -> String {
267        if self.is_option {
268            format!(
269                "Option<{}>",
270                if self.is_input {
271                    format!("&{}", JSVALUE)
272                } else {
273                    JSVALUE.to_owned()
274                }
275            )
276        } else {
277            if self.is_input {
278                format!("&{}", JSVALUE)
279            } else {
280                JSVALUE.to_owned()
281            }
282        }
283    }
284}
285
286fn strip_parenthesis(mut ts_type: &str) -> &str {
287    while ts_type.starts_with("(") && ts_type.ends_with(")") {
288        ts_type = &ts_type[1..ts_type.len() - 1].trim();
289    }
290    return ts_type;
291}
292
293/// Splits into correct comma delimited arguments
294fn split_into_args(ts_type: &str) -> Vec<&str> {
295    let mut depth_angle: u16 = 0;
296    let mut depth_square: u16 = 0;
297    let mut depth_paren: u16 = 0;
298    let mut splits = Vec::new();
299    let mut last: usize = 0;
300    for (i, c) in ts_type.char_indices() {
301        match c {
302            '<' => depth_angle += 1,
303            '>' => depth_angle = depth_angle.saturating_sub(1),
304            '[' => depth_square += 1,
305            ']' => depth_square = depth_square.saturating_sub(1),
306            '(' => depth_paren += 1,
307            ')' => depth_paren = depth_paren.saturating_sub(1),
308            ',' if depth_angle == 0 && depth_square == 0 && depth_paren == 0 => {
309                splits.push(ts_type[last..i].trim());
310                last = i + 1;
311            }
312            _ => {}
313        }
314    }
315    let len = ts_type.len();
316    if last != len {
317        let maybe_arg = ts_type[last..len].trim();
318        if !maybe_arg.is_empty() {
319            splits.push(maybe_arg);
320        }
321    }
322    splits
323}
324
325fn ts_type_to_rust_type(ts_type: Option<&str>, is_input: bool) -> RustType {
326    let Some(mut ts_type) = ts_type else {
327        return RustType::Regular(
328            (if is_input {
329                DEFAULT_GENRIC_INPUT
330            } else {
331                DEFAULT_GENERIC_OUTPUT
332            })
333            .to_owned(),
334        );
335    };
336    ts_type = strip_parenthesis(&mut ts_type);
337    if ts_type.starts_with("Promise<") && ts_type.ends_with(">") {
338        assert!(!is_input, "Promise cannot be used as input type");
339        ts_type = &ts_type[8..ts_type.len() - 1];
340    }
341    ts_type = strip_parenthesis(&mut ts_type);
342    if ts_type.contains(JSVALUE_START) {
343        let parts = split_top_level_union(ts_type);
344        let len = parts.len();
345        if len == 1 && parts[0].starts_with(JSVALUE_START) {
346            return RustType::JsValue(JsValue {
347                is_option: false,
348                is_input,
349            });
350        }
351
352        if len == 2 && parts.contains(&"null") {
353            return RustType::JsValue(JsValue {
354                is_option: true,
355                is_input,
356            });
357        } else {
358            panic!("Invalid use of `{}` for `{}`", JSVALUE_START, ts_type);
359        }
360    }
361    if ts_type.contains(RUST_CALLBACK_JS_START) {
362        if !ts_type.starts_with(RUST_CALLBACK_JS_START) {
363            panic!("Nested RustCallback is not valid: {}", ts_type);
364        }
365        assert!(is_input, "Cannot return a RustCallback: {}", ts_type);
366        let ts_type = &ts_type[RUST_CALLBACK_JS_START.len()..];
367        if !(ts_type.starts_with("<") && ts_type.ends_with(">")) {
368            panic!("Invalid RustCallback type: {}", ts_type);
369        }
370        let inner = &ts_type[1..ts_type.len() - 1];
371        let parts = split_into_args(inner);
372        let len = parts.len();
373        if len != 2 {
374            panic!(
375                "A RustCallback type expects two parameters, got: {:?}",
376                parts
377            );
378        }
379        let ts_input = parts[0];
380        let rs_input = if ts_input == "void" {
381            None
382        } else {
383            let rs_input = ts_type_to_rust_type_helper(ts_input, false);
384            if rs_input.is_none() || rs_input.as_ref().is_some_and(|e| e == UNIT) {
385                panic!("Type `{ts_input}` is not a valid input for `{RUST_CALLBACK_JS_START}`");
386            }
387            rs_input
388        };
389        let ts_output = parts[1];
390        let rs_output = if ts_output == "void" {
391            None
392        } else {
393            let rs_output = ts_type_to_rust_type_helper(ts_output, false);
394            if rs_output.is_none() || rs_output.as_ref().is_some_and(|e| e == UNIT) {
395                panic!("Type `{ts_output}` is not a valid output for `{RUST_CALLBACK_JS_START}`");
396            }
397            rs_output
398        };
399        return RustType::Callback(RustCallback {
400            input: rs_input,
401            output: rs_output,
402        });
403    }
404    RustType::Regular(match ts_type_to_rust_type_helper(ts_type, is_input) {
405        Some(value) => {
406            if value.contains(UNIT) && (is_input || &value != UNIT) {
407                // Would cause serialization errors since `serde_json::Value::Null` or any, cannot be deserialized into `()`.
408                // We handle `()` special case to account for this if this is the root type in the output. But not input or output nested.
409                panic!("`{}` is not valid in this position", ts_type);
410            }
411            value
412        }
413        None => (if is_input {
414            DEFAULT_GENRIC_INPUT
415        } else {
416            DEFAULT_GENERIC_OUTPUT
417        })
418        .to_owned(),
419    })
420}
421
422/// Returns None if could not determine type
423fn ts_type_to_rust_type_helper(mut ts_type: &str, can_be_ref: bool) -> Option<String> {
424    ts_type = ts_type.trim();
425    ts_type = strip_parenthesis(&mut ts_type);
426
427    let parts = split_top_level_union(ts_type);
428    if parts.len() > 1 {
429        // Handle single null union: T | null or null | T
430        if parts.len() == 2 && parts.contains(&"null") {
431            let inner = parts.iter().find(|p| **p != "null")?;
432            let inner_rust = ts_type_to_rust_type_helper(inner, can_be_ref)?;
433            return Some(format!("Option<{}>", inner_rust));
434        }
435        // Unsupported union type
436        return None;
437    }
438
439    ts_type = parts[0];
440
441    if ts_type.ends_with("[]") {
442        let inner = ts_type.strip_suffix("[]").unwrap();
443        let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
444        return Some(if can_be_ref {
445            format!("&[{}]", inner_rust)
446        } else {
447            format!("Vec<{}>", inner_rust)
448        });
449    }
450
451    if ts_type.starts_with("Array<") && ts_type.ends_with(">") {
452        let inner = &ts_type[6..ts_type.len() - 1];
453        let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
454        return Some(if can_be_ref {
455            format!("&[{}]", inner_rust)
456        } else {
457            format!("Vec<{}>", inner_rust)
458        });
459    }
460
461    if ts_type.starts_with("Set<") && ts_type.ends_with(">") {
462        let inner = &ts_type[4..ts_type.len() - 1];
463        let inner_rust = ts_type_to_rust_type_helper(inner, false)?;
464        if can_be_ref {
465            return Some(format!("&std::collections::HashSet<{}>", inner_rust));
466        } else {
467            return Some(format!("std::collections::HashSet<{}>", inner_rust));
468        }
469    }
470
471    if ts_type.starts_with("Map<") && ts_type.ends_with(">") {
472        let inner = &ts_type[4..ts_type.len() - 1];
473        let mut depth = 0;
474        let mut split_index = None;
475        for (i, c) in inner.char_indices() {
476            match c {
477                '<' => depth += 1,
478                '>' => depth -= 1,
479                ',' if depth == 0 => {
480                    split_index = Some(i);
481                    break;
482                }
483                _ => {}
484            }
485        }
486
487        if let Some(i) = split_index {
488            let (key, value) = inner.split_at(i);
489            let value = &value[1..]; // skip comma
490            let key_rust = ts_type_to_rust_type_helper(key.trim(), false)?;
491            let value_rust = ts_type_to_rust_type_helper(value.trim(), false)?;
492            if can_be_ref {
493                return Some(format!(
494                    "&std::collections::HashMap<{}, {}>",
495                    key_rust, value_rust
496                ));
497            } else {
498                return Some(format!(
499                    "std::collections::HashMap<{}, {}>",
500                    key_rust, value_rust
501                ));
502            }
503        } else {
504            return None;
505        }
506    }
507
508    // Base types
509    let rust_type = match ts_type {
510        "string" => {
511            if can_be_ref {
512                Some("&str".to_owned())
513            } else {
514                Some("String".to_owned())
515            }
516        }
517        "number" => Some("f64".to_owned()),
518        "boolean" => Some("bool".to_owned()),
519        "void" | "undefined" | "never" | "null" => Some(UNIT.to_owned()),
520        JSON => {
521            if can_be_ref {
522                Some(format!("&{SERDE_VALUE}"))
523            } else {
524                Some(SERDE_VALUE.to_owned())
525            }
526        }
527        "Promise" => {
528            panic!("`{}` - nested promises are not valid", ts_type)
529        }
530        // "any" | "unknown" | "object" | .. etc.
531        _ => None,
532    };
533
534    rust_type
535}
536
537/// Splits e.g. `number | null | string` ignoring nesting like `(number | null)[]`
538fn split_top_level_union(s: &str) -> Vec<&str> {
539    let mut parts = vec![];
540    let mut last = 0;
541    let mut depth_angle = 0;
542    let mut depth_paren = 0;
543
544    for (i, c) in s.char_indices() {
545        match c {
546            '<' => depth_angle += 1,
547            '>' => {
548                if depth_angle > 0 {
549                    depth_angle -= 1
550                }
551            }
552            '(' => depth_paren += 1,
553            ')' => {
554                if depth_paren > 0 {
555                    depth_paren -= 1
556                }
557            }
558            '|' if depth_angle == 0 && depth_paren == 0 => {
559                parts.push(s[last..i].trim());
560                last = i + 1;
561            }
562            _ => {}
563        }
564    }
565
566    if last < s.len() {
567        parts.push(s[last..].trim());
568    }
569
570    parts
571}
572
573fn type_to_string(ty: &Box<TsType>, source_map: &SourceMap) -> String {
574    let span = ty.span();
575    source_map
576        .span_to_snippet(span)
577        .expect("Could not get snippet from span for type")
578}
579
580fn function_pat_to_param_info<'a, I>(pats: I, source_map: &SourceMap) -> Vec<ParamInfo>
581where
582    I: Iterator<Item = &'a Pat>,
583{
584    pats.enumerate()
585        .map(|(i, pat)| to_param_info_helper(i, pat, source_map))
586        .collect()
587}
588
589fn to_param_info_helper(i: usize, pat: &Pat, source_map: &SourceMap) -> ParamInfo {
590    let name = if let Some(ident) = pat.as_ident() {
591        ident.id.sym.to_string()
592    } else {
593        format!("arg{}", i)
594    };
595
596    let js_type = pat
597        .as_ident()
598        .and_then(|ident| ident.type_ann.as_ref())
599        .map(|type_ann| {
600            let ty = &type_ann.type_ann;
601            type_to_string(ty, source_map)
602        });
603    let rust_type = ts_type_to_rust_type(js_type.as_deref(), true);
604
605    ParamInfo {
606        name,
607        js_type,
608        rust_type,
609    }
610}
611
612fn function_info_helper<'a, I>(
613    visitor: &JsVisitor,
614    name: String,
615    span: &swc_common::Span,
616    params: I,
617    return_type: Option<&Box<TsTypeAnn>>,
618    is_async: bool,
619    is_exported: bool,
620) -> FunctionInfo
621where
622    I: Iterator<Item = &'a Pat>,
623{
624    let doc_comment = visitor.extract_doc_comment(span);
625
626    let params = function_pat_to_param_info(params, &visitor.source_map);
627
628    let js_return_type = return_type.as_ref().map(|type_ann| {
629        let ty = &type_ann.type_ann;
630        type_to_string(ty, &visitor.source_map)
631    });
632    if !is_async
633        && let Some(ref js_return_type) = js_return_type
634        && js_return_type.starts_with("Promise")
635    {
636        panic!(
637            "Promise return type is only supported for async functions, use `async fn` instead. For `{js_return_type}`"
638        );
639    }
640
641    let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
642
643    FunctionInfo {
644        name,
645        ident: None,
646        params,
647        js_return_type,
648        rust_return_type,
649        is_exported,
650        is_async,
651        doc_comment,
652    }
653}
654
655impl Visit for JsVisitor {
656    /// Visit function declarations: function foo() {}
657    fn visit_fn_decl(&mut self, node: &FnDecl) {
658        let name = node.ident.sym.to_string();
659        self.functions.push(function_info_helper(
660            self,
661            name,
662            &node.span(),
663            node.function.params.iter().map(|e| &e.pat),
664            node.function.return_type.as_ref(),
665            node.function.is_async,
666            false,
667        ));
668        node.visit_children_with(self);
669    }
670
671    /// Visit function expressions: const foo = function() {}
672    fn visit_var_declarator(&mut self, node: &VarDeclarator) {
673        if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
674            if let Some(init) = &node.init {
675                let span = node.span();
676                let name = ident.id.sym.to_string();
677                match &**init {
678                    swc_ecma_ast::Expr::Fn(fn_expr) => {
679                        self.functions.push(function_info_helper(
680                            &self,
681                            name,
682                            &span,
683                            fn_expr.function.params.iter().map(|e| &e.pat),
684                            fn_expr.function.return_type.as_ref(),
685                            fn_expr.function.is_async,
686                            false,
687                        ));
688                    }
689                    swc_ecma_ast::Expr::Arrow(arrow_fn) => {
690                        self.functions.push(function_info_helper(
691                            &self,
692                            name,
693                            &span,
694                            arrow_fn.params.iter(),
695                            arrow_fn.return_type.as_ref(),
696                            arrow_fn.is_async,
697                            false,
698                        ));
699                    }
700                    _ => {}
701                }
702            }
703        }
704        node.visit_children_with(self);
705    }
706
707    /// Visit export declarations: export function foo() {} or export class Bar {}
708    fn visit_export_decl(&mut self, node: &ExportDecl) {
709        match &node.decl {
710            Decl::Fn(fn_decl) => {
711                let span = node.span();
712                let name = fn_decl.ident.sym.to_string();
713                self.functions.push(function_info_helper(
714                    &self,
715                    name,
716                    &span,
717                    fn_decl.function.params.iter().map(|e| &e.pat),
718                    fn_decl.function.return_type.as_ref(),
719                    fn_decl.function.is_async,
720                    true,
721                ));
722            }
723            Decl::Class(class_decl) => {
724                let name = class_decl.ident.sym.to_string();
725                let span = class_decl.class.span();
726                let doc_comment = self.extract_doc_comment(&span);
727                let mut methods = Vec::new();
728
729                for member in &class_decl.class.body {
730                    match member {
731                        ClassMember::Method(method) => {
732                            let method_name = match &method.key {
733                                PropName::Ident(ident) => ident.sym.to_string(),
734                                PropName::Str(str_lit) => str_lit.value.to_string(),
735                                _ => continue,
736                            };
737
738                            let method_span = method.span();
739                            let method_doc = self.extract_doc_comment(&method_span);
740
741                            let params = function_pat_to_param_info(
742                                method.function.params.iter().map(|p| &p.pat),
743                                &self.source_map,
744                            );
745
746                            let js_return_type =
747                                method.function.return_type.as_ref().map(|type_ann| {
748                                    let ty = &type_ann.type_ann;
749                                    type_to_string(ty, &self.source_map)
750                                });
751
752                            let is_async = method.function.is_async;
753                            if !is_async
754                                && js_return_type
755                                    .as_ref()
756                                    .is_some_and(|js_return_type: &String| {
757                                        js_return_type.starts_with("Promise")
758                                    })
759                            {
760                                panic!(
761                                    "Method `{}` in exported class `{}` returns a Promise but is not marked as async",
762                                    method_name, name
763                                );
764                            }
765
766                            let rust_return_type =
767                                ts_type_to_rust_type(js_return_type.as_deref(), false);
768
769                            methods.push(MethodInfo {
770                                name: method_name,
771                                params,
772                                js_return_type,
773                                rust_return_type,
774                                is_async,
775                                is_static: method.is_static,
776                                doc_comment: method_doc,
777                            });
778                        }
779                        _ => {}
780                    }
781                }
782
783                self.classes.push(ClassInfo {
784                    name,
785                    ident: None,
786                    methods,
787                    is_exported: true,
788                    doc_comment,
789                });
790            }
791            _ => {}
792        }
793        node.visit_children_with(self);
794    }
795
796    /// Visit named exports: export { foo }
797    fn visit_named_export(&mut self, node: &NamedExport) {
798        for spec in &node.specifiers {
799            if let ExportSpecifier::Named(named) = spec {
800                let original_name = named.orig.atom().to_string();
801                let out_name = named
802                    .exported
803                    .as_ref()
804                    .map(|e| e.atom().to_string())
805                    .unwrap_or_else(|| original_name.clone());
806
807                // These are visited after the declarations so if this is exporting a function or a class, it should be findable here
808                if let Some(func) = self.functions.iter_mut().find(|f| f.name == original_name) {
809                    let mut func = func.clone();
810                    func.name = out_name.clone();
811                    func.is_exported = true;
812                    self.functions.push(func);
813                }
814                if let Some(class) = self.classes.iter_mut().find(|c| c.name == original_name) {
815                    let mut class = class.clone();
816                    class.name = out_name.clone();
817                    class.is_exported = true;
818                    self.classes.push(class);
819                }
820            }
821        }
822        node.visit_children_with(self);
823    }
824
825    /// Visit class declarations: class Foo {}
826    fn visit_class_decl(&mut self, node: &ClassDecl) {
827        let name = node.ident.sym.to_string();
828        let span = node.span();
829        let doc_comment = self.extract_doc_comment(&span);
830        let mut methods = Vec::new();
831
832        for member in &node.class.body {
833            match member {
834                ClassMember::Method(method) => {
835                    let method_name = match &method.key {
836                        PropName::Ident(ident) => ident.sym.to_string(),
837                        PropName::Str(str_lit) => str_lit.value.to_string(),
838                        _ => continue,
839                    };
840
841                    let method_span = method.span();
842                    let method_doc = self.extract_doc_comment(&method_span);
843
844                    let params = function_pat_to_param_info(
845                        method.function.params.iter().map(|p| &p.pat),
846                        &self.source_map,
847                    );
848
849                    let js_return_type = method.function.return_type.as_ref().map(|type_ann| {
850                        let ty = &type_ann.type_ann;
851                        type_to_string(ty, &self.source_map)
852                    });
853
854                    let is_async = method.function.is_async;
855                    if !is_async
856                        && js_return_type
857                            .as_ref()
858                            .is_some_and(|js_return_type: &String| {
859                                js_return_type.starts_with("Promise")
860                            })
861                    {
862                        panic!(
863                            "Function `{}` in class `{}` returns a Promise but is not marked as async",
864                            method_name, name
865                        );
866                    }
867
868                    let rust_return_type = ts_type_to_rust_type(js_return_type.as_deref(), false);
869
870                    methods.push(MethodInfo {
871                        name: method_name,
872                        params,
873                        js_return_type,
874                        rust_return_type,
875                        is_async,
876                        is_static: method.is_static,
877                        doc_comment: method_doc,
878                    });
879                }
880                _ => {}
881            }
882        }
883
884        self.classes.push(ClassInfo {
885            name,
886            ident: None,
887            methods,
888            is_exported: false,
889            doc_comment,
890        });
891
892        node.visit_children_with(self);
893    }
894}
895
896fn parse_script_file(file_path: &Path, is_js: bool) -> Result<(Vec<FunctionInfo>, Vec<ClassInfo>)> {
897    let js_content = fs::read_to_string(file_path).map_err(|e| {
898        syn::Error::new(
899            proc_macro2::Span::call_site(),
900            format!("Could not read file '{}': {}", file_path.display(), e),
901        )
902    })?;
903
904    let source_map = SourceMap::default();
905    let fm = source_map.new_source_file(
906        swc_common::FileName::Custom(file_path.display().to_string()).into(),
907        js_content.clone(),
908    );
909    let comments = SingleThreadedComments::default();
910
911    // Enable TypeScript parsing to handle type annotations
912    let syntax = if is_js {
913        Syntax::Es(EsSyntax {
914            jsx: false,
915            fn_bind: false,
916            decorators: false,
917            decorators_before_export: false,
918            export_default_from: false,
919            import_attributes: false,
920            allow_super_outside_method: false,
921            allow_return_outside_function: false,
922            auto_accessors: false,
923            explicit_resource_management: false,
924        })
925    } else {
926        Syntax::Typescript(swc_ecma_parser::TsSyntax {
927            tsx: false,
928            decorators: false,
929            dts: false,
930            no_early_errors: false,
931            disallow_ambiguous_jsx_like: true,
932        })
933    };
934
935    let lexer = Lexer::new(
936        syntax,
937        Default::default(),
938        StringInput::from(&*fm),
939        Some(&comments),
940    );
941
942    let mut parser = Parser::new_from(lexer);
943
944    let module = parser.parse_module().map_err(|e| {
945        syn::Error::new(
946            proc_macro2::Span::call_site(),
947            format!(
948                "Failed to parse script file '{}': {:?}",
949                file_path.display(),
950                e
951            ),
952        )
953    })?;
954
955    let mut visitor = JsVisitor::new(comments, source_map);
956    module.visit_with(&mut visitor);
957
958    Ok((visitor.functions, visitor.classes))
959}
960
961fn get_types_to_generate(
962    classes: Vec<ClassInfo>,
963    functions: Vec<FunctionInfo>,
964    import_spec: &ImportSpec,
965    file: &Path,
966) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
967    fn named_helper(
968        names: &Vec<Ident>,
969        mut all_class_infos: Vec<ClassInfo>,
970        mut all_function_infos: Vec<FunctionInfo>,
971        file: &Path,
972    ) -> Result<(Vec<ClassInfo>, Vec<FunctionInfo>)> {
973        let mut resolved_function_infos = Vec::new();
974        let mut resolved_class_infos = Vec::new();
975        for name in names {
976            let name_str = name.to_string();
977
978            if let Some(pos) = all_function_infos
979                .iter()
980                .position(|f: &FunctionInfo| f.name == name_str && f.is_exported)
981            {
982                let mut function_info = all_function_infos.remove(pos);
983                function_info.ident.replace(name.clone());
984                resolved_function_infos.push(function_info);
985                continue;
986            }
987            if let Some(pos) = all_class_infos
988                .iter()
989                .position(|c: &ClassInfo| c.name == name_str && c.is_exported)
990            {
991                let mut class_info = all_class_infos.remove(pos);
992                class_info.ident.replace(name.clone());
993                resolved_class_infos.push(class_info);
994                continue;
995            }
996            if all_function_infos.iter().any(|f| f.name == name_str) {
997                return Err(syn::Error::new(
998                    proc_macro2::Span::call_site(),
999                    format!(
1000                        "Function '{}' not exported in file '{}'",
1001                        name,
1002                        file.display()
1003                    ),
1004                ));
1005            }
1006            if all_class_infos.iter().any(|c| c.name == name_str) {
1007                return Err(syn::Error::new(
1008                    proc_macro2::Span::call_site(),
1009                    format!("Class '{}' not exported in file '{}'", name, file.display()),
1010                ));
1011            }
1012            return Err(syn::Error::new(
1013                proc_macro2::Span::call_site(),
1014                format!(
1015                    "Function or Class '{}' not found in file '{}'",
1016                    name,
1017                    file.display()
1018                ),
1019            ));
1020        }
1021        Ok((resolved_class_infos, resolved_function_infos))
1022    }
1023    match import_spec {
1024        ImportSpec::All => Ok((
1025            classes.into_iter().filter(|e| e.is_exported).collect(),
1026            functions.into_iter().filter(|e| e.is_exported).collect(),
1027        )),
1028        ImportSpec::Single(name) => named_helper(&vec![name.clone()], classes, functions, file),
1029        ImportSpec::Named(names) => named_helper(names, classes, functions, file),
1030    }
1031}
1032
1033fn generate_class_wrapper(
1034    class_info: &ClassInfo,
1035    asset_path: &LitStr,
1036    function_id_hasher: &blake3::Hasher,
1037) -> TokenStream2 {
1038    let class_ident = class_info
1039        .ident
1040        .clone()
1041        .unwrap_or_else(|| Ident::new(class_info.name.as_str(), proc_macro2::Span::call_site()));
1042
1043    let doc_comment = if class_info.doc_comment.is_empty() {
1044        quote! {}
1045    } else {
1046        let doc_lines: Vec<_> = class_info
1047            .doc_comment
1048            .iter()
1049            .map(|line| quote! { #[doc = #line] })
1050            .collect();
1051        quote! { #(#doc_lines)* }
1052    };
1053
1054    let mut parts: Vec<TokenStream2> = Vec::new();
1055    for method in &class_info.methods {
1056        let func_info = FunctionInfo {
1057            name: method.name.clone(),
1058            ident: None,
1059            params: method.params.clone(),
1060            js_return_type: method.js_return_type.clone(),
1061            rust_return_type: method.rust_return_type.clone(),
1062            is_exported: true,
1063            is_async: method.is_async,
1064            doc_comment: method.doc_comment.clone(),
1065        };
1066
1067        let inner_function = generate_invocation(
1068            Some(FunctionClassContext {
1069                class_name: class_info.name.clone(),
1070                ident: class_ident.clone(),
1071                is_static: method.is_static,
1072            }),
1073            &func_info,
1074            asset_path,
1075            function_id_hasher,
1076        );
1077
1078        let method_name = format_ident!("{}", method.name);
1079        let method_params: Vec<_> = method
1080            .params
1081            .iter()
1082            .filter_map(|param| {
1083                if param.is_drop() {
1084                    return None;
1085                }
1086                let param_name = format_ident!("{}", param.name);
1087                let type_tokens = param.rust_type.to_tokens();
1088                Some(quote! { #param_name: #type_tokens })
1089            })
1090            .collect();
1091
1092        let param_names: Vec<_> = method
1093            .params
1094            .iter()
1095            .map(|p| format_ident!("{}", p.name))
1096            .collect();
1097
1098        let method_doc = if method.doc_comment.is_empty() {
1099            quote! {}
1100        } else {
1101            let doc_lines: Vec<_> = method
1102                .doc_comment
1103                .iter()
1104                .map(|line| quote! { #[doc = #line] })
1105                .collect();
1106            quote! { #(#doc_lines)* }
1107        };
1108
1109        fn returns_self_type(func_info: &FunctionInfo, class_info: &ClassInfo) -> bool {
1110            let Some(js_return_type) = &func_info.js_return_type else {
1111                return false;
1112            };
1113            if !matches!(func_info.rust_return_type, RustType::JsValue(_)) {
1114                return false;
1115            }
1116            if js_return_type.starts_with("JsValue<") && js_return_type.ends_with('>') {
1117                let inner = js_return_type[8..js_return_type.len() - 1].trim();
1118                if inner == class_info.name {
1119                    return true;
1120                }
1121            }
1122            return false;
1123        }
1124
1125        let (invocation, return_type, generic) = if returns_self_type(&func_info, &class_info) {
1126            // Constructs this
1127            let invocation = if method.is_static {
1128                quote! {
1129                    Ok(#class_ident::new(#method_name(#(#param_names),*).await?))
1130                }
1131            } else {
1132                quote! {
1133                    Ok(#class_ident::new(#method_name(&self.0, #(#param_names),*).await?))
1134                }
1135            };
1136            let (_, generic_tokens) = return_type_tokens(
1137                &method.rust_return_type,
1138                class_info.ident.as_ref().map(|e| e.span()),
1139            );
1140            let return_type_tokens = quote! { Result<#class_ident, dioxus_use_js::JsError> };
1141            (invocation, return_type_tokens, generic_tokens)
1142        } else {
1143            let invocation = if method.is_static {
1144                quote! {
1145                    #method_name(#(#param_names),*).await
1146                }
1147            } else {
1148                quote! {
1149                    #method_name(&self.0, #(#param_names),*).await
1150                }
1151            };
1152            let (return_type_tokens, generic_tokens) = return_type_tokens(
1153                &method.rust_return_type,
1154                class_info.ident.as_ref().map(|e| e.span()),
1155            );
1156            (invocation, return_type_tokens, generic_tokens)
1157        };
1158
1159        let part = if method.is_static {
1160            quote! {
1161                #method_doc
1162                #[allow(non_snake_case)]
1163                pub async fn #method_name #generic(#(#method_params),*) -> #return_type {
1164                    #[inline]
1165                    #inner_function
1166                    #invocation
1167                }
1168            }
1169        } else {
1170            quote! {
1171                #method_doc
1172                #[allow(non_snake_case)]
1173                pub async fn #method_name #generic(&self, #(#method_params),*) -> #return_type {
1174                    #[inline]
1175                    #inner_function
1176                    #invocation
1177                }
1178            }
1179        };
1180
1181        parts.push(part);
1182    }
1183
1184    quote! {
1185        #doc_comment
1186        #[derive(Clone, Debug, PartialEq, Eq, Hash,)]
1187        pub struct #class_ident(dioxus_use_js::JsValue);
1188
1189        impl #class_ident {
1190            pub fn new(js_value: dioxus_use_js::JsValue) -> Self {
1191                Self(js_value)
1192            }
1193        }
1194
1195        impl #class_ident {
1196            #(#parts)*
1197        }
1198
1199        impl AsRef<dioxus_use_js::JsValue> for #class_ident {
1200            fn as_ref(&self) -> &dioxus_use_js::JsValue {
1201                &self.0
1202            }
1203        }
1204    }
1205}
1206
1207struct FunctionClassContext {
1208    class_name: String,
1209    ident: Ident,
1210    is_static: bool,
1211}
1212
1213fn generate_invocation(
1214    class: Option<FunctionClassContext>,
1215    func: &FunctionInfo,
1216    asset_path: &LitStr,
1217    function_id_hasher: &blake3::Hasher,
1218) -> TokenStream2 {
1219    let is_class_method = class.as_ref().is_some_and(|e| !e.is_static);
1220    let mut params = func.params.clone();
1221    if is_class_method {
1222        let new_param = ParamInfo {
1223            name: "_m_".to_owned(),
1224            js_type: None,
1225            rust_type: RustType::JsValue(JsValue {
1226                is_option: false,
1227                is_input: true,
1228            }),
1229        };
1230        params.insert(0, new_param);
1231    }
1232    // If we have callbacks, we cant do a simpl return, we have to do message passing
1233    let mut callback_name_to_index: HashMap<String, u64> = HashMap::new();
1234    let mut callback_name_to_info: IndexMap<String, &RustCallback> = IndexMap::new();
1235    let mut index: u64 = 0;
1236    let mut needs_drop = false;
1237    let mut has_callbacks = false;
1238    for param in &params {
1239        if let RustType::Callback(callback) = &param.rust_type {
1240            callback_name_to_index.insert(param.name.to_owned(), index);
1241            index += 1;
1242            callback_name_to_info.insert(param.name.to_owned(), callback);
1243            has_callbacks = true;
1244            needs_drop = true;
1245        } else if param.is_drop() {
1246            needs_drop = true;
1247        }
1248    }
1249    let func_name_str = &func.name;
1250    let func_name_static_ident = quote! { FUNC_NAME };
1251
1252    let send_calls: Vec<TokenStream2> = params
1253        .iter()
1254        .flat_map(|param| {
1255            if param.is_drop() {
1256                return None;
1257            }
1258            let param_name = format_ident!("{}", param.name);
1259            match &param.rust_type {
1260                RustType::Regular(_) => Some(quote! {
1261                    eval.send(#param_name).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1262                }),
1263                RustType::JsValue(js_value) => {
1264                    if js_value.is_option {
1265                        Some(quote! {
1266                            #[allow(deprecated)]
1267                            eval.send(#param_name.map(|e| e.internal_get())).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1268                        })
1269                    } else {
1270                        Some(quote! {
1271                            #[allow(deprecated)]
1272                            eval.send(#param_name.internal_get()).map_err(|e| dioxus_use_js::JsError::Eval { func: #func_name_static_ident, error: std::sync::Arc::new(e) })?;
1273                        })
1274                    }
1275                },
1276                RustType::Callback(_) => {
1277                    None
1278                },
1279            }
1280        })
1281        .collect();
1282
1283    // Note we use `func.params` here
1284    let call_params = &func
1285        .params
1286        .iter()
1287        .map(|p| p.name.as_str())
1288        .collect::<Vec<&str>>()
1289        .join(", ");
1290    let prepare = if has_callbacks {
1291        assert!(needs_drop);
1292        "let _i_=\"**INVOCATION_ID**\";let _l_={};window[_i_]=_l_;let _g_ = 0;let _a_=true;const _c_=(c, v)=>{if(!_a_){return Promise.reject(new Error(\"Channel already destroyed\"));}_g_+=1;if(_g_>Number.MAX_SAFE_INTEGER){_g_= 0;}let o, e;let p=new Promise((rs, rj)=>{o=rs;e=rj});_l_[_g_]=[o, e];dioxus.send([c,_g_,v]);return p;};"
1293    } else if needs_drop {
1294        "let _i_=\"**INVOCATION_ID**\";"
1295    } else {
1296        ""
1297    };
1298    let param_declarations = &params
1299        .iter()
1300        .map(|param| {
1301            if needs_drop && param.is_drop() {
1302                return format!("let {}=_dp_;", param.name);
1303            }
1304            match &param.rust_type {
1305            RustType::Regular(_) => {
1306                format!("let {}=await dioxus.recv();", param.name)
1307            }
1308            RustType::JsValue(js_value) => {
1309                let param_name = &param.name;
1310                if js_value.is_option {
1311                format!(
1312                    "let _{param_name}T_=await dioxus.recv();let {param_name}=null;if(_{param_name}T_!==null){{{param_name}=window[_{param_name}T_]}};",
1313                )
1314            }
1315            else {
1316                format!(
1317                    "let _{param_name}T_=await dioxus.recv();let {param_name}=window[_{param_name}T_];",
1318                )
1319            }
1320            },
1321            RustType::Callback(rust_callback) => {
1322                let name = &param.name;
1323                let index = callback_name_to_index.get(name).unwrap();
1324                let RustCallback { input, output } = rust_callback;
1325                match (input, output) {
1326                    (None, None) => {
1327                        // no return, but still need to await ack
1328                        format!(
1329                            "const {}=async()=>{{await _c_({},null);}};",
1330                            name, index
1331                        )
1332                    },
1333                    (None, Some(_)) => {
1334                        format!(
1335                            "const {}=async()=>{{return await _c_({},null);}};",
1336                            name, index
1337
1338                        )
1339                    },
1340                    (Some(_), None) => {
1341                        // no return, but still need to await ack
1342                        format!(
1343                            "const {}=async(v)=>{{await _c_({},v);}};",
1344                            name, index
1345                        )
1346                    },
1347                    (Some(_), Some(_)) => {
1348                        format!(
1349                            "const {}=async(v)=>{{return await _c_({},v);}};",
1350                            name, index
1351                        )
1352                    },
1353                }
1354            },
1355        }})
1356        .collect::<Vec<_>>()
1357        .join("");
1358    let mut maybe_await = String::new();
1359    if func.is_async {
1360        maybe_await.push_str("await");
1361    }
1362    let func_call_full_path = if is_class_method {
1363        let var_name = &params.first().unwrap().name;
1364        format!("{var_name}.{func_name_str}")
1365    } else if let Some(class) = &class {
1366        let class_name = &class.class_name;
1367        format!("{class_name}.{func_name_str}")
1368    } else {
1369        func_name_str.to_owned()
1370    };
1371    let call_function = match &func.rust_return_type {
1372        RustType::Regular(_) => {
1373            format!("return [true, {maybe_await} {func_call_full_path}({call_params})];")
1374        }
1375        RustType::Callback(_) => {
1376            unreachable!("This cannot be an output type, the macro should have panicked earlier.")
1377        }
1378        RustType::JsValue(js_value) => {
1379            let check = if js_value.is_option {
1380                // null or undefined is valid, since this is e.g. `Option<JsValue>`
1381                "if (_v_===null||_v_===undefined){return [true,null];}".to_owned()
1382            } else {
1383                format!(
1384                    "if (_v_===null||_v_===undefined){{console.error(\"The result of `{func_call_full_path}` was null or undefined, but a value is needed for JsValue\");return [true,null];}}"
1385                )
1386            };
1387            format!(
1388                "const _v_={maybe_await} {func_call_full_path}({call_params});{check}let _j_=\"__js-value-\"+crypto.randomUUID();window[_j_]=_v_;return [true,_j_];"
1389            )
1390        }
1391    };
1392    let drop_declare = if needs_drop {
1393        // Note the additional `d` also added by `SignalDrop`
1394        "let _d_;let _dp_=new Promise((r)=>_d_=r);window[_i_+\"d\"]=_d_;"
1395    } else {
1396        ""
1397    };
1398    let drop_handle = if needs_drop {
1399        if has_callbacks {
1400            "(async()=>{await _dp_;dioxus.close();_a_=false;let w=window[_i_];delete window[_i_];for(const[o, e] of Object.values(w)){e(new Error(\"Channel destroyed\"));}})();"
1401        } else {
1402            "(async()=>{await _dp_;dioxus.close();})();"
1403        }
1404    } else {
1405        assert!(
1406            !has_callbacks,
1407            "If this is true then needing drop should be true"
1408        );
1409        ""
1410    };
1411    let finally = if needs_drop {
1412        ""
1413    } else {
1414        "finally{dioxus.close();}"
1415    };
1416    let asset_path_string = asset_path.value();
1417    // Note: eval will fail if returning undefined. undefined happens if there is no return type
1418    let js = if is_class_method {
1419        format!(
1420            "{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1421        )
1422    } else if let Some(class) = &class {
1423        let class_name = &class.class_name;
1424        format!(
1425            "const{{{class_name}}}=await import(\"{asset_path_string}\");{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1426        )
1427    } else {
1428        assert_eq!(func_call_full_path.as_str(), func_name_str);
1429        format!(
1430            "const{{{func_name_str}}}=await import(\"{asset_path_string}\");{prepare}{drop_declare}{param_declarations}{drop_handle}try{{{call_function}}}catch(e){{console.warn(\"Executing `{func_call_full_path}` threw:\", e);return [false,null];}}{finally}"
1431        )
1432    };
1433    fn to_raw_string_literal(s: &str) -> Literal {
1434        let mut hashes = String::from("#");
1435        while s.contains(&format!("\"{}", hashes)) {
1436            hashes.push('#');
1437        }
1438
1439        let raw = format!("r{h}\"{s}\"{h}", h = hashes);
1440        Literal::from_str(&raw).unwrap()
1441    }
1442    let comment = to_raw_string_literal(&js);
1443    // Easier debugging to see what the generated js is. Will be compiled away.
1444    let js_in_comment = quote! {
1445        #[doc = #comment]
1446        fn ___above_is_the_generated_js___() {}
1447    };
1448    let js_format = js.replace("{", "{{").replace("}", "}}");
1449    let js_format = if is_class_method {
1450        assert!(!js_format.contains(&asset_path_string));
1451        js_format
1452    } else {
1453        js_format.replace(&asset_path_string, "{}")
1454    };
1455    let js_format = if needs_drop {
1456        js_format.replace("**INVOCATION_ID**", "{}")
1457    } else {
1458        js_format
1459    };
1460    let js_eval_statement = if needs_drop {
1461        let js_line = if is_class_method {
1462            quote! {
1463            let js = format!(#js_format, &invocation_id);
1464            }
1465        } else {
1466            quote! {
1467                const MODULE: Asset = asset!(#asset_path);
1468                let js = format!(#js_format, MODULE, &invocation_id);
1469            }
1470        };
1471        let function_id = {
1472            let mut hasher = function_id_hasher.clone();
1473            hasher.update(func_call_full_path.as_bytes());
1474            let mut output_reader = hasher.finalize_xof();
1475            let mut truncated_bytes = vec![0u8; 10];
1476            use std::io::Read;
1477            output_reader.read_exact(&mut truncated_bytes).unwrap();
1478            let function_id =
1479                base64::engine::general_purpose::STANDARD_NO_PAD.encode(truncated_bytes);
1480            function_id
1481        };
1482        quote! {
1483            static INVOCATION_NUM: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1484            // Each invocation id guarentees a unique namespace for the callback invocation for requests/responses and on drop everything there can be cleaned up and outstanding promises rejected.
1485            let invocation_id = format!("__{}{}", #function_id, INVOCATION_NUM.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
1486            #js_line
1487            let mut eval = dioxus::document::eval(js.as_str());
1488        }
1489    } else {
1490        if is_class_method {
1491            quote! {
1492                let js = #js_format;
1493                let mut eval = dioxus::document::eval(js);
1494            }
1495        } else {
1496            quote! {
1497                const MODULE: Asset = asset!(#asset_path);
1498                let js = format!(#js_format, MODULE);
1499                let mut eval = dioxus::document::eval(js.as_str());
1500            }
1501        }
1502    };
1503
1504    // Generate parameter types with extracted type information
1505    let param_types: Vec<_> = params
1506        .iter()
1507        .filter_map(|param| {
1508            if param.is_drop() {
1509                return None;
1510            }
1511            let param_name = format_ident!("{}", param.name);
1512            let type_tokens = param.rust_type.to_tokens();
1513            Some(quote! { #param_name: #type_tokens })
1514        })
1515        .collect();
1516
1517    let (return_type_tokens, generic_tokens) = return_type_tokens(
1518        &func.rust_return_type,
1519        func.ident.as_ref().map(|e| e.span()),
1520    );
1521
1522    // Generate documentation comment if available - preserve original JSDoc format
1523    let doc_comment = if func.doc_comment.is_empty() {
1524        quote! {}
1525    } else {
1526        let doc_lines: Vec<_> = func
1527            .doc_comment
1528            .iter()
1529            .map(|line| quote! { #[doc = #line] })
1530            .collect();
1531        quote! { #(#doc_lines)* }
1532    };
1533
1534    let func_name = func
1535        .ident
1536        .clone()
1537        // Can not exist if `::*`
1538        .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
1539
1540    // void like returns always send back "Null" as an ack
1541    let void_output_mapping = if func.rust_return_type.to_string() == UNIT {
1542        quote! {
1543            .and_then(|e| {
1544                if matches!(e, dioxus_use_js::SerdeJsonValue::Null) {
1545                    Ok(())
1546                } else {
1547                    Err(dioxus_use_js::JsError::Eval {
1548                        func: #func_name_static_ident,
1549                        error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(
1550                            <dioxus_use_js::SerdeJsonError as dioxus_use_js::SerdeDeError>::custom(dioxus_use_js::__BAD_VOID_RETURN.to_owned())
1551                        ))
1552                    })
1553                }
1554            })
1555        }
1556    } else {
1557        quote! {}
1558    };
1559
1560    let callback_arms: Vec<TokenStream2> = callback_name_to_index
1561        .iter()
1562        .map(|(name, index)| {
1563            let callback_name = format_ident!("{}", name);
1564            let callback_info = callback_name_to_info.get(name).unwrap();
1565            let callback_call = match (&callback_info.input, &callback_info.output) {
1566                (None, None) => {
1567                    quote! {
1568                    dioxus::prelude::spawn({let responder = responder.clone(); async move {
1569                        let result = #callback_name(()).await;
1570
1571                        match result {
1572                            // send ack
1573                            Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1574                            Err(error) => responder.respond(request_id, false, error),
1575                        }
1576                    }});
1577                    }
1578                },
1579                (None, Some(_)) => {
1580                    quote! {
1581                    dioxus::prelude::spawn({let responder = responder.clone(); async move {
1582                        let result = #callback_name(()).await;
1583
1584                        match result {
1585                            Ok(value) => responder.respond(request_id, true, value),
1586                            Err(error) => responder.respond(request_id, false, error),
1587                        }
1588                    }});
1589                }
1590                },
1591                (Some(_), None) => {
1592                    quote! {
1593                    let value = values.next().unwrap();
1594                    let value = match dioxus_use_js::serde_json_from_value(value) {
1595                        Ok(value) => value,
1596                        Err(value) => {
1597                            responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1598                            continue;
1599                        }
1600                    };
1601
1602                    dioxus::prelude::spawn({let responder = responder.clone(); async move {
1603                        let result = #callback_name(value).await;
1604
1605                        match result {
1606                            // send ack
1607                            Ok(_) => responder.respond(request_id, true, dioxus_use_js::SerdeJsonValue::Null),
1608                            Err(error) => responder.respond(request_id, false, error),
1609                        }
1610                    }});
1611                }
1612                },
1613                (Some(_), Some(_)) => {
1614                    quote! {
1615                    let value = values.next().unwrap();
1616                    let value = match dioxus_use_js::serde_json_from_value(value) {
1617                        Ok(value) => value,
1618                        Err(value) => {
1619                            responder.respond(request_id, false, dioxus_use_js::SerdeJsonValue::String(dioxus_use_js::__UNEXPECTED_CALLBACK_TYPE.to_owned()));
1620                            continue;
1621                        }
1622                    };
1623
1624                    dioxus::prelude::spawn({let responder = responder.clone(); async move {
1625                        let result = #callback_name(value).await;
1626
1627                        match result {
1628                            Ok(value) => responder.respond(request_id, true, value),
1629                            Err(error) => responder.respond(request_id, false, error),
1630                        }
1631                    }});
1632                }
1633                }
1634            };
1635            quote! {
1636                #index => {
1637                    #callback_call
1638                }
1639            }
1640        })
1641        .collect();
1642
1643    let callback_spawn = if !callback_arms.is_empty() {
1644        quote! {
1645            dioxus::prelude::spawn({
1646                    async move {
1647                        let responder = dioxus_use_js::CallbackResponder::new(&invocation_id);
1648                        let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id.clone());
1649                        loop {
1650                            let result = eval.recv::<dioxus_use_js::SerdeJsonValue>().await;
1651                            let value = match result {
1652                                Ok(v) => v,
1653                                Err(e) => {
1654                                    // Though we still may be able to accept more callback requests,
1655                                    // We shutdown otherwise the invocation of this callback will be awaiting forever
1656                                    // (since we don't know where the request came from so we cannot cancel it).
1657                                    dioxus::prelude::error!(
1658                                        "Callback receiver errored. Shutting down all callbacks for invocation id `{}`: {:?}",
1659                                        &invocation_id,
1660                                        e
1661                                    );
1662                                    return;
1663                                }
1664                            };
1665                            let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1666                                unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1667                            };
1668                            let len = values.len();
1669                            if len != 3 {
1670                                unreachable!("{}", dioxus_use_js::__CALLBACK_SEND_VALIDATION_MSG);
1671                            }
1672                            let mut values = values.into_iter();
1673                            let action = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1674                            let request_id = values.next().unwrap().as_u64().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1675                            match action {
1676                                #(#callback_arms,)*
1677                                _ => unreachable!("{}", dioxus_use_js::__BAD_CALL_MSG),
1678                            }
1679                        }
1680                    }
1681            });
1682        }
1683    } else if needs_drop {
1684        // We can't use `use_drop` because the rule of hooks, so like the callback case, we just
1685        // spawn a future that will never finish, but the eval will drop and fire off the signal
1686        // when the component drops.
1687        quote! {
1688            dioxus::prelude::spawn(async move {
1689                let _signal_drop = dioxus_use_js::SignalDrop::new(invocation_id);
1690                let f = dioxus_use_js::PendingFuture;
1691                f.await;
1692            });
1693        }
1694    } else {
1695        quote! {}
1696    };
1697
1698    let end_statement = quote! {
1699        let value = eval.await.map_err(|e| {
1700            dioxus_use_js::JsError::Eval {
1701                func: #func_name_static_ident,
1702                error: std::sync::Arc::new(e),
1703            }
1704        })?;
1705        let dioxus_use_js::SerdeJsonValue::Array(values) = value else {
1706            unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1707        };
1708        if values.len() != 2 {
1709            unreachable!("{}", dioxus_use_js::__RESULT_SEND_VALIDATION_MSG);
1710        }
1711        let mut values = values.into_iter();
1712        let success = values.next().unwrap().as_bool().expect(dioxus_use_js::__INDEX_VALIDATION_MSG);
1713        if success {
1714            let value = values.next().unwrap();
1715            return dioxus_use_js::serde_json_from_value(value).map_err(|e| {
1716                dioxus_use_js::JsError::Eval {
1717                    func: #func_name_static_ident,
1718                    error: std::sync::Arc::new(dioxus::document::EvalError::Serialization(e)),
1719                }
1720            })
1721            #void_output_mapping;
1722        } else {
1723             return Err(dioxus_use_js::JsError::Threw { func: #func_name_static_ident });
1724        }
1725    };
1726
1727    quote! {
1728        #doc_comment
1729        #[allow(non_snake_case)]
1730        pub async fn #func_name #generic_tokens(#(#param_types),*) -> #return_type_tokens {
1731            const #func_name_static_ident: &str = #func_name_str;
1732            #js_in_comment
1733            #js_eval_statement
1734            #(#send_calls)*
1735            #callback_spawn
1736            #end_statement
1737        }
1738    }
1739}
1740
1741fn return_type_tokens(
1742    return_type: &RustType,
1743    span: Option<proc_macro2::Span>,
1744) -> (proc_macro2::TokenStream, Option<proc_macro2::TokenStream>) {
1745    let span = span.unwrap_or_else(|| proc_macro2::Span::call_site());
1746    let parsed_type = return_type.to_tokens();
1747    if return_type.to_string() == DEFAULT_GENERIC_OUTPUT {
1748        let generic = Ident::new(DEFAULT_GENERIC_OUTPUT, span);
1749        let generic_decl: TypeParam = syn::parse_str(DEFAULT_OUTPUT_GENERIC_DECLARTION).unwrap();
1750        (
1751            quote! { Result<#generic, dioxus_use_js::JsError> },
1752            Some(quote! { <#generic_decl> }),
1753        )
1754    } else {
1755        (
1756            quote! { Result<#parsed_type, dioxus_use_js::JsError> },
1757            None,
1758        )
1759    }
1760}
1761
1762/// 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.
1763#[proc_macro]
1764pub fn use_js(input: TokenStream) -> TokenStream {
1765    let input = parse_macro_input!(input as UseJsInput);
1766
1767    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
1768        Ok(dir) => dir,
1769        Err(_) => {
1770            return TokenStream::from(
1771                syn::Error::new(
1772                    proc_macro2::Span::call_site(),
1773                    "CARGO_MANIFEST_DIR environment variable not found",
1774                )
1775                .to_compile_error(),
1776            );
1777        }
1778    };
1779
1780    let UseJsInput {
1781        js_bundle_path,
1782        ts_source_path,
1783        import_spec,
1784    } = input;
1785
1786    let js_file_path = std::path::Path::new(&manifest_dir).join(js_bundle_path.value());
1787
1788    let (js_all_functions, js_all_classes) = match parse_script_file(&js_file_path, true) {
1789        Ok(result) => result,
1790        Err(e) => return TokenStream::from(e.to_compile_error()),
1791    };
1792
1793    let (js_classes_to_generate, js_functions_to_generate) = match get_types_to_generate(
1794        js_all_classes,
1795        js_all_functions,
1796        &import_spec,
1797        &js_file_path,
1798    ) {
1799        Ok((classes, funcs)) => (classes, funcs),
1800        Err(e) => {
1801            return TokenStream::from(e.to_compile_error());
1802        }
1803    };
1804
1805    let (functions_to_generate, classes_to_generate) = if let Some(ts_file_path) = ts_source_path {
1806        let ts_file_path = std::path::Path::new(&manifest_dir).join(ts_file_path.value());
1807        let (ts_all_functions, ts_all_classes) = match parse_script_file(&ts_file_path, false) {
1808            Ok(result) => result,
1809            Err(e) => return TokenStream::from(e.to_compile_error()),
1810        };
1811
1812        let (ts_classes_to_generate, ts_functions_to_generate) = match get_types_to_generate(
1813            ts_all_classes,
1814            ts_all_functions,
1815            &import_spec,
1816            &ts_file_path,
1817        ) {
1818            Ok((classes, funcs)) => (classes, funcs),
1819            Err(e) => {
1820                return TokenStream::from(e.to_compile_error());
1821            }
1822        };
1823
1824        for ts_func in ts_functions_to_generate.iter() {
1825            if let Some(js_func) = js_functions_to_generate
1826                .iter()
1827                .find(|f| f.name == ts_func.name)
1828            {
1829                if ts_func.params.len() != js_func.params.len() {
1830                    return TokenStream::from(syn::Error::new(
1831                        proc_macro2::Span::call_site(),
1832                        format!(
1833                            "Function '{}' has different parameter count in JS and TS files. Bundle may be out of date",
1834                            ts_func.name
1835                        ),
1836                    )
1837                    .to_compile_error());
1838                }
1839            } else {
1840                return TokenStream::from(syn::Error::new(
1841                    proc_macro2::Span::call_site(),
1842                    format!(
1843                        "Function '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1844                        ts_func.name
1845                    ),
1846                )
1847                .to_compile_error());
1848            }
1849        }
1850
1851        // Validate classes match between TS and JS
1852        for ts_class in ts_classes_to_generate.iter() {
1853            if let Some(js_class) = js_classes_to_generate
1854                .iter()
1855                .find(|c| c.name == ts_class.name)
1856            {
1857                if ts_class.methods.len() != js_class.methods.len() {
1858                    return TokenStream::from(syn::Error::new(
1859                        proc_macro2::Span::call_site(),
1860                        format!(
1861                            "Class '{}' has different method count in JS and TS files. Bundle may be out of date",
1862                            ts_class.name
1863                        ),
1864                    )
1865                    .to_compile_error());
1866                }
1867            } else {
1868                return TokenStream::from(syn::Error::new(
1869                    proc_macro2::Span::call_site(),
1870                    format!(
1871                        "Class '{}' is defined in TS file but not in JS file. Bundle may be out of date",
1872                        ts_class.name
1873                    ),
1874                )
1875                .to_compile_error());
1876            }
1877        }
1878
1879        (ts_functions_to_generate, ts_classes_to_generate)
1880    } else {
1881        (js_functions_to_generate, js_classes_to_generate)
1882    };
1883
1884    for function in functions_to_generate.iter() {
1885        for param in function.params.iter() {
1886            if param.name.starts_with("_") && param.name.ends_with("_") {
1887                panic!(
1888                    "Parameter name '{}' in function '{}' is invalid. Parameters starting and ending with underscores are reserved.",
1889                    param.name, function.name
1890                );
1891            }
1892            if param.name == "dioxus" {
1893                panic!(
1894                    "Parameter name 'dioxus' in function '{}' is invalid. This parameter name is reserved.",
1895                    function.name
1896                );
1897            }
1898            if param.name == function.name {
1899                panic!(
1900                    "Parameter name '{}' in function '{}' is invalid. Parameters cannot have the same name as the function.",
1901                    param.name, function.name
1902                );
1903            }
1904        }
1905    }
1906
1907    let call_site_span = proc_macro::Span::call_site();
1908    let file = call_site_span.file();
1909    let line_number = call_site_span.line();
1910    let column_number = call_site_span.column();
1911    let mut unhashed_id = file;
1912    unhashed_id.push_str(":");
1913    unhashed_id.push_str(&line_number.to_string());
1914    unhashed_id.push_str(":");
1915    unhashed_id.push_str(&column_number.to_string());
1916    unhashed_id.push_str(":");
1917    let mut function_id_hasher = blake3::Hasher::new();
1918    function_id_hasher.update(unhashed_id.as_bytes());
1919
1920    let function_wrappers: Vec<TokenStream2> = functions_to_generate
1921        .iter()
1922        .map(|func| generate_invocation(None, func, &js_bundle_path, &function_id_hasher))
1923        .collect();
1924
1925    let class_wrappers: Vec<TokenStream2> = classes_to_generate
1926        .iter()
1927        .map(|class| generate_class_wrapper(class, &js_bundle_path, &function_id_hasher))
1928        .collect();
1929
1930    let expanded = quote! {
1931        #(#function_wrappers)*
1932        #(#class_wrappers)*
1933    };
1934
1935    TokenStream::from(expanded)
1936}
1937
1938//************************************************************************//
1939
1940#[cfg(test)]
1941mod tests {
1942    use super::*;
1943
1944    #[test]
1945    fn test_primitives() {
1946        assert_eq!(
1947            ts_type_to_rust_type(Some("string"), false).to_string(),
1948            "String"
1949        );
1950        assert_eq!(
1951            ts_type_to_rust_type(Some("string"), true).to_string(),
1952            "&str"
1953        );
1954        assert_eq!(
1955            ts_type_to_rust_type(Some("number"), false).to_string(),
1956            "f64"
1957        );
1958        assert_eq!(
1959            ts_type_to_rust_type(Some("number"), true).to_string(),
1960            "f64"
1961        );
1962        assert_eq!(
1963            ts_type_to_rust_type(Some("boolean"), false).to_string(),
1964            "bool"
1965        );
1966        assert_eq!(
1967            ts_type_to_rust_type(Some("boolean"), true).to_string(),
1968            "bool"
1969        );
1970    }
1971
1972    #[test]
1973    fn test_nullable_primitives() {
1974        assert_eq!(
1975            ts_type_to_rust_type(Some("string | null"), true).to_string(),
1976            "Option<&str>"
1977        );
1978        assert_eq!(
1979            ts_type_to_rust_type(Some("string | null"), false).to_string(),
1980            "Option<String>"
1981        );
1982        assert_eq!(
1983            ts_type_to_rust_type(Some("number | null"), true).to_string(),
1984            "Option<f64>"
1985        );
1986        assert_eq!(
1987            ts_type_to_rust_type(Some("number | null"), false).to_string(),
1988            "Option<f64>"
1989        );
1990        assert_eq!(
1991            ts_type_to_rust_type(Some("boolean | null"), true).to_string(),
1992            "Option<bool>"
1993        );
1994        assert_eq!(
1995            ts_type_to_rust_type(Some("boolean | null"), false).to_string(),
1996            "Option<bool>"
1997        );
1998    }
1999
2000    #[test]
2001    fn test_arrays() {
2002        assert_eq!(
2003            ts_type_to_rust_type(Some("string[]"), true).to_string(),
2004            "&[String]"
2005        );
2006        assert_eq!(
2007            ts_type_to_rust_type(Some("string[]"), false).to_string(),
2008            "Vec<String>"
2009        );
2010        assert_eq!(
2011            ts_type_to_rust_type(Some("Array<number>"), true).to_string(),
2012            "&[f64]"
2013        );
2014        assert_eq!(
2015            ts_type_to_rust_type(Some("Array<number>"), false).to_string(),
2016            "Vec<f64>"
2017        );
2018    }
2019
2020    #[test]
2021    fn test_nullable_array_elements() {
2022        assert_eq!(
2023            ts_type_to_rust_type(Some("(string | null)[]"), true).to_string(),
2024            "&[Option<String>]"
2025        );
2026        assert_eq!(
2027            ts_type_to_rust_type(Some("(string | null)[]"), false).to_string(),
2028            "Vec<Option<String>>"
2029        );
2030        assert_eq!(
2031            ts_type_to_rust_type(Some("Array<number | null>"), true).to_string(),
2032            "&[Option<f64>]"
2033        );
2034        assert_eq!(
2035            ts_type_to_rust_type(Some("Array<number | null>"), false).to_string(),
2036            "Vec<Option<f64>>"
2037        );
2038    }
2039
2040    #[test]
2041    fn test_nullable_array_itself() {
2042        assert_eq!(
2043            ts_type_to_rust_type(Some("string[] | null"), true).to_string(),
2044            "Option<&[String]>"
2045        );
2046        assert_eq!(
2047            ts_type_to_rust_type(Some("string[] | null"), false).to_string(),
2048            "Option<Vec<String>>"
2049        );
2050        assert_eq!(
2051            ts_type_to_rust_type(Some("Array<number> | null"), true).to_string(),
2052            "Option<&[f64]>"
2053        );
2054        assert_eq!(
2055            ts_type_to_rust_type(Some("Array<number> | null"), false).to_string(),
2056            "Option<Vec<f64>>"
2057        );
2058    }
2059
2060    #[test]
2061    fn test_nullable_array_and_elements() {
2062        assert_eq!(
2063            ts_type_to_rust_type(Some("Array<string | null> | null"), true).to_string(),
2064            "Option<&[Option<String>]>"
2065        );
2066        assert_eq!(
2067            ts_type_to_rust_type(Some("Array<string | null> | null"), false).to_string(),
2068            "Option<Vec<Option<String>>>"
2069        );
2070    }
2071
2072    #[test]
2073    fn test_fallback_for_union() {
2074        assert_eq!(
2075            ts_type_to_rust_type(Some("string | number"), true).to_string(),
2076            "impl dioxus_use_js::SerdeSerialize"
2077        );
2078        assert_eq!(
2079            ts_type_to_rust_type(Some("string | number"), false).to_string(),
2080            "DeserializeOwned"
2081        );
2082        assert_eq!(
2083            ts_type_to_rust_type(Some("string | number | null"), true).to_string(),
2084            "impl dioxus_use_js::SerdeSerialize"
2085        );
2086        assert_eq!(
2087            ts_type_to_rust_type(Some("string | number | null"), false).to_string(),
2088            "DeserializeOwned"
2089        );
2090    }
2091
2092    #[test]
2093    fn test_unknown_types() {
2094        assert_eq!(
2095            ts_type_to_rust_type(Some("foo"), true).to_string(),
2096            "impl dioxus_use_js::SerdeSerialize"
2097        );
2098        assert_eq!(
2099            ts_type_to_rust_type(Some("foo"), false).to_string(),
2100            "DeserializeOwned"
2101        );
2102
2103        assert_eq!(
2104            ts_type_to_rust_type(Some("any"), true).to_string(),
2105            "impl dioxus_use_js::SerdeSerialize"
2106        );
2107        assert_eq!(
2108            ts_type_to_rust_type(Some("any"), false).to_string(),
2109            "DeserializeOwned"
2110        );
2111        assert_eq!(
2112            ts_type_to_rust_type(Some("object"), true).to_string(),
2113            "impl dioxus_use_js::SerdeSerialize"
2114        );
2115        assert_eq!(
2116            ts_type_to_rust_type(Some("object"), false).to_string(),
2117            "DeserializeOwned"
2118        );
2119        assert_eq!(
2120            ts_type_to_rust_type(Some("unknown"), true).to_string(),
2121            "impl dioxus_use_js::SerdeSerialize"
2122        );
2123        assert_eq!(
2124            ts_type_to_rust_type(Some("unknown"), false).to_string(),
2125            "DeserializeOwned"
2126        );
2127
2128        assert_eq!(ts_type_to_rust_type(Some("void"), false).to_string(), "()");
2129        assert_eq!(
2130            ts_type_to_rust_type(Some("undefined"), false).to_string(),
2131            "()"
2132        );
2133        assert_eq!(ts_type_to_rust_type(Some("null"), false).to_string(), "()");
2134    }
2135
2136    #[test]
2137    fn test_extra_whitespace() {
2138        assert_eq!(
2139            ts_type_to_rust_type(Some("  string | null  "), true).to_string(),
2140            "Option<&str>"
2141        );
2142        assert_eq!(
2143            ts_type_to_rust_type(Some("  string | null  "), false).to_string(),
2144            "Option<String>"
2145        );
2146        assert_eq!(
2147            ts_type_to_rust_type(Some(" Array< string > "), true).to_string(),
2148            "&[String]"
2149        );
2150        assert_eq!(
2151            ts_type_to_rust_type(Some(" Array< string > "), false).to_string(),
2152            "Vec<String>"
2153        );
2154    }
2155
2156    #[test]
2157    fn test_map_types() {
2158        assert_eq!(
2159            ts_type_to_rust_type(Some("Map<string, number>"), true).to_string(),
2160            "&std::collections::HashMap<String, f64>"
2161        );
2162        assert_eq!(
2163            ts_type_to_rust_type(Some("Map<string, number>"), false).to_string(),
2164            "std::collections::HashMap<String, f64>"
2165        );
2166        assert_eq!(
2167            ts_type_to_rust_type(Some("Map<string, boolean>"), true).to_string(),
2168            "&std::collections::HashMap<String, bool>"
2169        );
2170        assert_eq!(
2171            ts_type_to_rust_type(Some("Map<string, boolean>"), false).to_string(),
2172            "std::collections::HashMap<String, bool>"
2173        );
2174        assert_eq!(
2175            ts_type_to_rust_type(Some("Map<number, string>"), true).to_string(),
2176            "&std::collections::HashMap<f64, String>"
2177        );
2178        assert_eq!(
2179            ts_type_to_rust_type(Some("Map<number, string>"), false).to_string(),
2180            "std::collections::HashMap<f64, String>"
2181        );
2182    }
2183
2184    #[test]
2185    fn test_set_types() {
2186        assert_eq!(
2187            ts_type_to_rust_type(Some("Set<string>"), true).to_string(),
2188            "&std::collections::HashSet<String>"
2189        );
2190        assert_eq!(
2191            ts_type_to_rust_type(Some("Set<string>"), false).to_string(),
2192            "std::collections::HashSet<String>"
2193        );
2194        assert_eq!(
2195            ts_type_to_rust_type(Some("Set<number>"), true).to_string(),
2196            "&std::collections::HashSet<f64>"
2197        );
2198        assert_eq!(
2199            ts_type_to_rust_type(Some("Set<number>"), false).to_string(),
2200            "std::collections::HashSet<f64>"
2201        );
2202        assert_eq!(
2203            ts_type_to_rust_type(Some("Set<boolean>"), true).to_string(),
2204            "&std::collections::HashSet<bool>"
2205        );
2206        assert_eq!(
2207            ts_type_to_rust_type(Some("Set<boolean>"), false).to_string(),
2208            "std::collections::HashSet<bool>"
2209        );
2210    }
2211
2212    #[test]
2213    fn test_rust_callback() {
2214        assert_eq!(
2215            ts_type_to_rust_type(Some("RustCallback<number,string>"), true).to_string(),
2216            "dioxus::core::Callback<f64, impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2217        );
2218        assert_eq!(
2219            ts_type_to_rust_type(Some("RustCallback<void,string>"), true).to_string(),
2220            "dioxus::core::Callback<(), impl Future<Output = Result<String, dioxus_use_js::SerdeJsonValue>> + 'static>"
2221        );
2222        assert_eq!(
2223            ts_type_to_rust_type(Some("RustCallback<void,void>"), true).to_string(),
2224            "dioxus::core::Callback<(), impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2225        );
2226        assert_eq!(
2227            ts_type_to_rust_type(Some("RustCallback<number,void>"), true).to_string(),
2228            "dioxus::core::Callback<f64, impl Future<Output = Result<(), dioxus_use_js::SerdeJsonValue>> + 'static>"
2229        );
2230    }
2231
2232    #[test]
2233    fn test_promise_types() {
2234        assert_eq!(
2235            ts_type_to_rust_type(Some("Promise<string>"), false).to_string(),
2236            "String"
2237        );
2238        assert_eq!(
2239            ts_type_to_rust_type(Some("Promise<number>"), false).to_string(),
2240            "f64"
2241        );
2242        assert_eq!(
2243            ts_type_to_rust_type(Some("Promise<boolean>"), false).to_string(),
2244            "bool"
2245        );
2246    }
2247
2248    #[test]
2249    fn test_json_types() {
2250        assert_eq!(
2251            ts_type_to_rust_type(Some("Json"), true).to_string(),
2252            "&dioxus_use_js::SerdeJsonValue"
2253        );
2254        assert_eq!(
2255            ts_type_to_rust_type(Some("Json"), false).to_string(),
2256            "dioxus_use_js::SerdeJsonValue"
2257        );
2258    }
2259
2260    #[test]
2261    fn test_js_value() {
2262        assert_eq!(
2263            ts_type_to_rust_type(Some("JsValue"), true).to_string(),
2264            "&dioxus_use_js::JsValue"
2265        );
2266        assert_eq!(
2267            ts_type_to_rust_type(Some("JsValue"), false).to_string(),
2268            "dioxus_use_js::JsValue"
2269        );
2270        assert_eq!(
2271            ts_type_to_rust_type(Some("JsValue<CustomType>"), true).to_string(),
2272            "&dioxus_use_js::JsValue"
2273        );
2274        assert_eq!(
2275            ts_type_to_rust_type(Some("JsValue<CustomType>"), false).to_string(),
2276            "dioxus_use_js::JsValue"
2277        );
2278
2279        assert_eq!(
2280            ts_type_to_rust_type(Some("Promise<JsValue>"), false).to_string(),
2281            "dioxus_use_js::JsValue"
2282        );
2283
2284        assert_eq!(
2285            ts_type_to_rust_type(Some("Promise<JsValue | null>"), false).to_string(),
2286            "Option<dioxus_use_js::JsValue>"
2287        );
2288        assert_eq!(
2289            ts_type_to_rust_type(Some("JsValue | null"), true).to_string(),
2290            "Option<&dioxus_use_js::JsValue>"
2291        );
2292        assert_eq!(
2293            ts_type_to_rust_type(Some("JsValue | null"), false).to_string(),
2294            "Option<dioxus_use_js::JsValue>"
2295        );
2296    }
2297
2298    #[test]
2299    fn test_class_parsing() {
2300        let ts_content = r#"
2301            /**
2302             * A test class
2303             */
2304            export class MyClass {
2305                constructor(name: string, value: number) {}
2306                
2307                /**
2308                 * Instance method
2309                 */
2310                greet(greeting: string): string {
2311                    return greeting;
2312                }
2313                
2314                /**
2315                 * Async method
2316                 */
2317                async fetchData(url: string): Promise<string> {
2318                    return "data";
2319                }
2320                
2321                /**
2322                 * Static method
2323                 */
2324                static create(): MyClass {
2325                    return new MyClass("test", 0);
2326                }
2327            }
2328        "#;
2329
2330        let source_map = SourceMap::default();
2331        let fm = source_map.new_source_file(
2332            swc_common::FileName::Custom("test.ts".to_string()).into(),
2333            ts_content.to_string(),
2334        );
2335        let comments = SingleThreadedComments::default();
2336
2337        let syntax = Syntax::Typescript(swc_ecma_parser::TsSyntax {
2338            tsx: false,
2339            decorators: false,
2340            dts: false,
2341            no_early_errors: false,
2342            disallow_ambiguous_jsx_like: true,
2343        });
2344
2345        let lexer = Lexer::new(
2346            syntax,
2347            Default::default(),
2348            StringInput::from(&*fm),
2349            Some(&comments),
2350        );
2351
2352        let mut parser = Parser::new_from(lexer);
2353        let module = parser.parse_module().unwrap();
2354
2355        let mut visitor = JsVisitor::new(comments, source_map);
2356        module.visit_with(&mut visitor);
2357
2358        // Dedup classes (as done in parse_script_file)
2359        visitor
2360            .classes
2361            .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
2362
2363        // Verify we parsed the class
2364        assert_eq!(visitor.classes.len(), 1);
2365        let class = &visitor.classes[0];
2366        assert_eq!(class.name, "MyClass");
2367        assert_eq!(class.is_exported, true);
2368
2369        // Verify methods
2370        assert_eq!(class.methods.len(), 3);
2371
2372        let greet = &class.methods[0];
2373        assert_eq!(greet.name, "greet");
2374        assert_eq!(greet.is_async, false);
2375        assert_eq!(greet.is_static, false);
2376        assert_eq!(greet.params.len(), 1);
2377        assert_eq!(greet.params[0].name, "greeting");
2378        assert_eq!(greet.params[0].rust_type.to_string(), "&str");
2379        assert_eq!(greet.rust_return_type.to_string(), "String");
2380
2381        let fetch_data = &class.methods[1];
2382        assert_eq!(fetch_data.name, "fetchData");
2383        assert_eq!(fetch_data.is_async, true);
2384        assert_eq!(fetch_data.is_static, false);
2385        assert_eq!(fetch_data.params.len(), 1);
2386        assert_eq!(fetch_data.rust_return_type.to_string(), "String");
2387
2388        let create = &class.methods[2];
2389        assert_eq!(create.name, "create");
2390        assert_eq!(create.is_async, false);
2391        assert_eq!(create.is_static, true);
2392        assert_eq!(create.params.len(), 0);
2393        // Note: The return type MyClass would be parsed as JsValue or unknown type
2394    }
2395}