dioxus_use_js/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{format_ident, quote};
4use std::{fs, path::Path};
5use swc_common::comments::{CommentKind, Comments};
6use swc_common::Spanned;
7use swc_common::{comments::SingleThreadedComments, SourceMap, Span};
8use swc_ecma_ast::{
9    Decl, ExportDecl, ExportSpecifier, FnDecl, ModuleExportName, NamedExport, Param, Pat,
10    VarDeclarator,
11};
12use swc_ecma_parser::EsSyntax;
13use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
14use swc_ecma_visit::{Visit, VisitWith};
15use syn::{
16    parse::{Parse, ParseStream},
17    parse_macro_input, Ident, LitStr, Result, Token,
18};
19
20#[derive(Debug, Clone)]
21enum ImportSpec {
22    /// *
23    All,
24    /// {greeting, other_func}
25    Named(Vec<Ident>),
26    /// greeting
27    Single(Ident),
28}
29
30struct UseJsInput {
31    asset_path: LitStr,
32    import_spec: ImportSpec,
33}
34
35impl Parse for UseJsInput {
36    fn parse(input: ParseStream) -> Result<Self> {
37        let asset_path: LitStr = input.parse()?;
38        input.parse::<Token![::]>()?;
39
40        let import_spec = if input.peek(Token![*]) {
41            input.parse::<Token![*]>()?;
42            ImportSpec::All
43        } else if input.peek(syn::token::Brace) {
44            let content;
45            syn::braced!(content in input);
46            let mut functions = Vec::new();
47
48            loop {
49                let ident: Ident = content.parse()?;
50                functions.push(ident);
51
52                if content.peek(Token![,]) {
53                    content.parse::<Token![,]>()?;
54                    if content.is_empty() {
55                        break;
56                    }
57                } else {
58                    break;
59                }
60            }
61
62            ImportSpec::Named(functions)
63        } else {
64            let ident: Ident = input.parse()?;
65            ImportSpec::Single(ident)
66        };
67
68        Ok(UseJsInput {
69            asset_path,
70            import_spec,
71        })
72    }
73}
74
75#[derive(Debug, Clone)]
76struct FunctionInfo {
77    name: String,
78    /// If specified in the use declaration
79    name_ident: Option<Ident>,
80    params: Vec<String>,
81    is_exported: bool,
82    /// The stripped lines
83    doc_comment: Vec<String>,
84}
85
86struct FunctionVisitor {
87    functions: Vec<FunctionInfo>,
88    comments: SingleThreadedComments,
89}
90
91impl FunctionVisitor {
92    fn new(comments: SingleThreadedComments) -> Self {
93        Self {
94            functions: Vec::new(),
95            comments,
96        }
97    }
98
99    fn extract_doc_comment(&self, span: Span) -> Vec<String> {
100        // Get leading comments for the span
101        let leading_comment = self.comments.get_leading(span.lo());
102
103        if let Some(comments) = leading_comment {
104            let mut doc_lines = Vec::new();
105
106            for comment in comments.iter() {
107                let comment_text = &comment.text;
108                match comment.kind {
109                    // Handle `///`. `//` is already stripped
110                    CommentKind::Line => {
111                        if let Some(content) = comment_text.strip_prefix("/") {
112                            let cleaned = content.trim_start();
113                            doc_lines.push(cleaned.to_string());
114                        }
115                    }
116                    // Handle `/*` `*/`. `/*` `*/` is already stripped
117                    CommentKind::Block => {
118                        for line in comment_text.lines() {
119                            if let Some(cleaned) = line.trim_start().strip_prefix("*") {
120                                doc_lines.push(cleaned.to_string());
121                            }
122                        }
123                    }
124                };
125            }
126
127            doc_lines
128        } else {
129            Vec::new()
130        }
131    }
132}
133
134fn function_params_to_names(params: &[Param]) -> Vec<String> {
135    params
136        .iter()
137        .enumerate()
138        .map(|(i, param)| {
139            if let Some(ident) = param.pat.as_ident() {
140                ident.id.sym.to_string()
141            } else {
142                format!("arg{}", i)
143            }
144        })
145        .collect()
146}
147
148fn function_pat_to_names(pats: &[Pat]) -> Vec<String> {
149    pats.iter()
150        .enumerate()
151        .map(|(i, pat)| {
152            if let Some(ident) = pat.as_ident() {
153                ident.id.sym.to_string()
154            } else {
155                format!("arg{}", i)
156            }
157        })
158        .collect()
159}
160
161impl Visit for FunctionVisitor {
162    /// Visit function declarations: function foo() {}
163    fn visit_fn_decl(&mut self, node: &FnDecl) {
164        let doc_comment = self.extract_doc_comment(node.span());
165
166        self.functions.push(FunctionInfo {
167            name: node.ident.sym.to_string(),
168            name_ident: None,
169            params: function_params_to_names(&node.function.params),
170            is_exported: false,
171            doc_comment,
172        });
173        node.visit_children_with(self);
174    }
175
176    /// Visit function expressions: const foo = function() {}
177    fn visit_var_declarator(&mut self, node: &VarDeclarator) {
178        if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
179            if let Some(init) = &node.init {
180                let doc_comment = self.extract_doc_comment(node.span());
181
182                match &**init {
183                    swc_ecma_ast::Expr::Fn(fn_expr) => {
184                        self.functions.push(FunctionInfo {
185                            name: ident.id.sym.to_string(),
186                            name_ident: None,
187                            params: function_params_to_names(&fn_expr.function.params),
188                            is_exported: false,
189                            doc_comment,
190                        });
191                    }
192                    swc_ecma_ast::Expr::Arrow(arrow_fn) => {
193                        self.functions.push(FunctionInfo {
194                            name: ident.id.sym.to_string(),
195                            name_ident: None,
196                            params: function_pat_to_names(&arrow_fn.params),
197                            is_exported: false,
198                            doc_comment,
199                        });
200                    }
201                    _ => {}
202                }
203            }
204        }
205        node.visit_children_with(self);
206    }
207
208    /// Visit export declarations: export function foo() {}
209    fn visit_export_decl(&mut self, node: &ExportDecl) {
210        if let Decl::Fn(fn_decl) = &node.decl {
211            let doc_comment = self.extract_doc_comment(node.span());
212
213            self.functions.push(FunctionInfo {
214                name: fn_decl.ident.sym.to_string(),
215                name_ident: None,
216                params: function_params_to_names(&fn_decl.function.params),
217                is_exported: true,
218                doc_comment,
219            });
220        }
221        node.visit_children_with(self);
222    }
223
224    /// Visit named exports: export { foo }
225    fn visit_named_export(&mut self, node: &NamedExport) {
226        for spec in &node.specifiers {
227            if let ExportSpecifier::Named(named) = spec {
228                let name = match &named.orig {
229                    ModuleExportName::Ident(ident) => ident.sym.to_string(),
230                    ModuleExportName::Str(str_lit) => str_lit.value.to_string(),
231                };
232
233                if let Some(func) = self.functions.iter_mut().find(|f| f.name == name) {
234                    func.is_exported = true;
235                }
236            }
237        }
238        node.visit_children_with(self);
239    }
240}
241
242fn parse_js_file(file_path: &Path) -> Result<Vec<FunctionInfo>> {
243    let js_content = fs::read_to_string(file_path).map_err(|e| {
244        syn::Error::new(
245            proc_macro2::Span::call_site(),
246            format!(
247                "Could not read JavaScript file '{}': {}",
248                file_path.display(),
249                e
250            ),
251        )
252    })?;
253
254    let cm = SourceMap::default();
255    let fm = cm.new_source_file(
256        swc_common::FileName::Custom(file_path.display().to_string()).into(),
257        js_content.clone(),
258    );
259    let comments = SingleThreadedComments::default();
260    let lexer = Lexer::new(
261        Syntax::Es(EsSyntax::default()),
262        Default::default(),
263        StringInput::from(&*fm),
264        Some(&comments),
265    );
266
267    let mut parser = Parser::new_from(lexer);
268
269    let module = parser.parse_module().map_err(|e| {
270        syn::Error::new(
271            proc_macro2::Span::call_site(),
272            format!(
273                "Failed to parse JavaScript file '{}': {:?}",
274                file_path.display(),
275                e
276            ),
277        )
278    })?;
279
280    let mut visitor = FunctionVisitor::new(comments);
281    module.visit_with(&mut visitor);
282
283    // Functions are added twice for some reason
284    visitor
285        .functions
286        .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str());
287    Ok(visitor.functions)
288}
289
290fn remove_function_info(name: &str, functions: &mut Vec<FunctionInfo>) -> Result<FunctionInfo> {
291    if let Some(pos) = functions.iter().position(|f| f.name == name) {
292        Ok(functions.remove(pos))
293    } else {
294        Err(syn::Error::new(
295            proc_macro2::Span::call_site(),
296            format!("Function '{}' not found in JavaScript file", name),
297        ))
298    }
299}
300
301fn get_functions_to_generate(
302    mut functions: Vec<FunctionInfo>,
303    import_spec: ImportSpec,
304) -> Result<Vec<FunctionInfo>> {
305    match import_spec {
306        ImportSpec::All => Ok(functions),
307        ImportSpec::Single(name) => {
308            let mut func = remove_function_info(name.to_string().as_str(), &mut functions)?;
309            func.name_ident.replace(name);
310            Ok(vec![func])
311        }
312        ImportSpec::Named(names) => {
313            let mut result = Vec::new();
314            for name in names {
315                let mut func = remove_function_info(name.to_string().as_str(), &mut functions)?;
316                func.name_ident.replace(name);
317                result.push(func);
318            }
319            Ok(result)
320        }
321    }
322}
323
324fn generate_function_wrapper(func: &FunctionInfo, asset_path: &LitStr) -> TokenStream2 {
325    let send_calls: Vec<TokenStream2> = func
326        .params
327        .iter()
328        .map(|param| {
329            let param = format_ident!("{}", param);
330            quote! {
331                eval.send(#param)?;
332            }
333        })
334        .collect();
335
336    let js_func_name = &func.name;
337    let mut js_format = format!(r#"const {{{{ {js_func_name} }}}} = await import("{{}}");"#);
338    for param in func.params.iter() {
339        js_format.push_str(&format!("\nlet {} = await dioxus.recv();", param));
340    }
341    js_format.push_str(&format!("\nreturn {}(", js_func_name));
342    for (i, param) in func.params.iter().enumerate() {
343        if i > 0 {
344            js_format.push_str(", ");
345        }
346        js_format.push_str(param.as_str());
347    }
348    js_format.push_str(");");
349
350    let param_types: Vec<_> = func
351        .params
352        .iter()
353        .map(|param| {
354            let param = format_ident!("{}", param);
355            quote! { #param: impl serde::Serialize }
356        })
357        .collect();
358
359    // Generate documentation comment if available - preserve original JSDoc format
360    let doc_comment = if func.doc_comment.is_empty() {
361        quote! {}
362    } else {
363        let doc_lines: Vec<_> = func
364            .doc_comment
365            .iter()
366            .map(|line| quote! { #[doc = #line] })
367            .collect();
368        quote! { #(#doc_lines)* }
369    };
370
371    let func_name = func
372        .name_ident
373        .clone()
374        // Can not exist if `::*`
375        .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site()));
376    quote! {
377        #doc_comment
378        #[allow(non_snake_case)]
379        pub async fn #func_name(#(#param_types),*) -> Result<serde_json::Value, document::EvalError> {
380            const MODULE: Asset = asset!(#asset_path);
381            let js = format!(#js_format, MODULE);
382            let eval = document::eval(js.as_str());
383            #(#send_calls)*
384            eval.await
385        }
386    }
387}
388
389/// A macro to create rust binding to javascript functions.
390///```rust,ignore
391/// use dioxus::prelude::*;
392///
393/// // Generate the greeting function at compile time
394/// use_js!("examples/assets/example.js"::greeting);
395///
396///  // Or generate multiple functions:
397///  // use_js!("examples/assets/example.js"::{greeting, add});
398///
399///  // Or generate all exported functions:
400///  // use_js!("examples/assets/example.js"::*);
401///
402/// fn main() {
403///     launch(App);
404/// }
405///
406/// #[component]
407/// fn App() -> Element {
408///     let future = use_resource(|| async move {
409///         let from = "dave";
410///         let to = "john";
411///
412///         // Now we can call the generated function directly!
413///         let greeting_result = greeting(from, to)
414///             .await
415///             .map_err(Box::<dyn std::error::Error>::from)?;
416///         let greeting: String =
417///             serde_json::from_value(greeting_result).map_err(Box::<dyn std::error::Error>::from)?;
418///         Ok::<String, Box<dyn std::error::Error>>(greeting)
419///     });
420///
421///     rsx!(
422///         div {
423///             h1 { "Dioxus `use_js!` macro example!" }
424///             {
425///                 match &*future.read() {
426///                     Some(Ok(greeting)) => rsx! {
427///                         p { "Greeting from JavaScript: {greeting}" }
428///                     },
429///                     Some(Err(e)) => rsx! {
430///                         p { "Error: {e}" }
431///                     },
432///                     None => rsx! {
433///                         p { "Running js..." }
434///                     },
435///                 }
436///             }
437///         }
438///     )
439/// }
440/// ```
441#[proc_macro]
442pub fn use_js(input: TokenStream) -> TokenStream {
443    let input = parse_macro_input!(input as UseJsInput);
444
445    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
446        Ok(dir) => dir,
447        Err(_) => {
448            return TokenStream::from(
449                syn::Error::new(
450                    proc_macro2::Span::call_site(),
451                    "CARGO_MANIFEST_DIR environment variable not found",
452                )
453                .to_compile_error(),
454            );
455        }
456    };
457
458    let asset_path = &input.asset_path;
459    let js_file_path = std::path::Path::new(&manifest_dir).join(asset_path.value());
460
461    let all_functions = match parse_js_file(&js_file_path) {
462        Ok(funcs) => funcs,
463        Err(e) => return TokenStream::from(e.to_compile_error()),
464    };
465
466    let import_spec = input.import_spec;
467    let functions_to_generate = match get_functions_to_generate(all_functions, import_spec) {
468        Ok(funcs) => funcs,
469        Err(e) => return TokenStream::from(e.to_compile_error()),
470    };
471
472    let function_wrappers: Vec<TokenStream2> = functions_to_generate
473        .iter()
474        .map(|func| generate_function_wrapper(func, asset_path))
475        .collect();
476
477    let expanded = quote! {
478        #(#function_wrappers)*
479    };
480
481    TokenStream::from(expanded)
482}