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