at main 481 lines 15 kB view raw
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 pub async fn #func_name(#(#param_types),*) -> Result<serde_json::Value, document::EvalError> { 379 const MODULE: Asset = asset!(#asset_path); 380 let js = format!(#js_format, MODULE); 381 let eval = document::eval(js.as_str()); 382 #(#send_calls)* 383 eval.await 384 } 385 } 386} 387 388/// A macro to create rust binding to javascript functions. 389///```rust,ignore 390/// use dioxus::prelude::*; 391/// 392/// // Generate the greeting function at compile time 393/// use_js!("examples/assets/example.js"::greeting); 394/// 395/// // Or generate multiple functions: 396/// // use_js!("examples/assets/example.js"::{greeting, add}); 397/// 398/// // Or generate all exported functions: 399/// // use_js!("examples/assets/example.js"::*); 400/// 401/// fn main() { 402/// launch(App); 403/// } 404/// 405/// #[component] 406/// fn App() -> Element { 407/// let future = use_resource(|| async move { 408/// let from = "dave"; 409/// let to = "john"; 410/// 411/// // Now we can call the generated function directly! 412/// let greeting_result = greeting(from, to) 413/// .await 414/// .map_err(Box::<dyn std::error::Error>::from)?; 415/// let greeting: String = 416/// serde_json::from_value(greeting_result).map_err(Box::<dyn std::error::Error>::from)?; 417/// Ok::<String, Box<dyn std::error::Error>>(greeting) 418/// }); 419/// 420/// rsx!( 421/// div { 422/// h1 { "Dioxus `use_js!` macro example!" } 423/// { 424/// match &*future.read() { 425/// Some(Ok(greeting)) => rsx! { 426/// p { "Greeting from JavaScript: {greeting}" } 427/// }, 428/// Some(Err(e)) => rsx! { 429/// p { "Error: {e}" } 430/// }, 431/// None => rsx! { 432/// p { "Running js..." } 433/// }, 434/// } 435/// } 436/// } 437/// ) 438/// } 439/// ``` 440#[proc_macro] 441pub fn use_js(input: TokenStream) -> TokenStream { 442 let input = parse_macro_input!(input as UseJsInput); 443 444 let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") { 445 Ok(dir) => dir, 446 Err(_) => { 447 return TokenStream::from( 448 syn::Error::new( 449 proc_macro2::Span::call_site(), 450 "CARGO_MANIFEST_DIR environment variable not found", 451 ) 452 .to_compile_error(), 453 ); 454 } 455 }; 456 457 let asset_path = &input.asset_path; 458 let js_file_path = std::path::Path::new(&manifest_dir).join(asset_path.value()); 459 460 let all_functions = match parse_js_file(&js_file_path) { 461 Ok(funcs) => funcs, 462 Err(e) => return TokenStream::from(e.to_compile_error()), 463 }; 464 465 let import_spec = input.import_spec; 466 let functions_to_generate = match get_functions_to_generate(all_functions, import_spec) { 467 Ok(funcs) => funcs, 468 Err(e) => return TokenStream::from(e.to_compile_error()), 469 }; 470 471 let function_wrappers: Vec<TokenStream2> = functions_to_generate 472 .iter() 473 .map(|func| generate_function_wrapper(func, asset_path)) 474 .collect(); 475 476 let expanded = quote! { 477 #(#function_wrappers)* 478 }; 479 480 TokenStream::from(expanded) 481}