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