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 All,
24 Named(Vec<Ident>),
26 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 name_ident: Option<Ident>,
80 params: Vec<String>,
81 is_exported: bool,
82 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 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 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 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 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 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 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 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 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 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 .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#[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}