Skip to content

Commit 61d21ad

Browse files
committed
Move call_js macro over from dioxus_sdk
DioxusLabs/sdk#87
1 parent f610c6b commit 61d21ad

File tree

5 files changed

+370
-0
lines changed

5 files changed

+370
-0
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ members = [
7272
"packages/asset-resolver",
7373
"packages/depinfo",
7474
"packages/server",
75+
"packages/use-js-macro",
7576

7677
# Playwright tests
7778
"packages/playwright-tests/liveview",

packages/use-js-macro/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "use-js-macro"
3+
edition = "2024"
4+
version.workspace = true
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
proc-macro2 = "1.0"
11+
quote = "1.0"
12+
syn = { version = "2.0", features = ["full"] }
13+
swc_ecma_parser = "17"
14+
swc_ecma_ast = "13"
15+
swc_ecma_visit = "13"
16+
swc_common = "13"

packages/use-js-macro/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Dioxus call-js
2+
3+
A macro to simplify calling javascript from rust
4+
5+
## Usage
6+
Add `dioxus-call-js` to your `Cargo.toml`:
7+
```toml
8+
[dependencies]
9+
dioxus-call-js = "0.1"
10+
```
11+
12+
Example:
13+
```rust
14+
use dioxus::prelude::*;
15+
use dioxus_call_js::call_js;
16+
17+
fn main() {
18+
launch(App);
19+
}
20+
21+
#[component]
22+
fn App() -> Element {
23+
let future = use_resource(|| async move {
24+
let from = "dave";
25+
let to = "john";
26+
let greeting = call_js!("assets/example.js", greeting(from, to)).await.unwrap();
27+
let greeting: String = serde_json::from_value(greeting).unwrap();
28+
return greeting;
29+
});
30+
31+
rsx!(
32+
div {
33+
h1 { "Dioxus `call_js!` macro example!" }
34+
{
35+
match &*future.read() {
36+
Some(greeting) => rsx! {
37+
p { "Greeting from JavaScript: {greeting}" }
38+
},
39+
None => rsx! {
40+
p { "Running js" }
41+
},
42+
}
43+
}
44+
}
45+
)
46+
}
47+
```

packages/use-js-macro/src/lib.rs

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::TokenStream as TokenStream2;
3+
use quote::quote;
4+
use std::sync::Arc;
5+
use std::{fs, path::Path};
6+
use swc_common::SourceMap;
7+
use swc_ecma_ast::{
8+
Decl, ExportDecl, ExportSpecifier, FnDecl, ModuleExportName, NamedExport, VarDeclarator,
9+
};
10+
use swc_ecma_parser::EsSyntax;
11+
use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
12+
use swc_ecma_visit::{Visit, VisitWith};
13+
use syn::{
14+
Expr, ExprCall, LitStr, Result, Token,
15+
parse::{Parse, ParseStream},
16+
parse_macro_input,
17+
};
18+
19+
struct CallJsInput {
20+
asset_path: LitStr,
21+
function_call: ExprCall,
22+
}
23+
24+
impl Parse for CallJsInput {
25+
fn parse(input: ParseStream) -> Result<Self> {
26+
let asset_path: LitStr = input.parse()?;
27+
input.parse::<Token![,]>()?;
28+
29+
let function_call: ExprCall = input.parse()?;
30+
31+
Ok(CallJsInput {
32+
asset_path,
33+
function_call,
34+
})
35+
}
36+
}
37+
38+
fn extract_function_name(call: &ExprCall) -> Result<String> {
39+
match &*call.func {
40+
Expr::Path(path) => {
41+
if let Some(ident) = path.path.get_ident() {
42+
Ok(ident.to_string())
43+
} else {
44+
Err(syn::Error::new_spanned(
45+
&path.path,
46+
"Function call must be a simple identifier",
47+
))
48+
}
49+
}
50+
_ => Err(syn::Error::new_spanned(
51+
&call.func,
52+
"Function call must be a simple identifier",
53+
)),
54+
}
55+
}
56+
57+
#[derive(Debug)]
58+
struct FunctionInfo {
59+
name: String,
60+
param_count: usize,
61+
is_exported: bool,
62+
}
63+
64+
struct FunctionVisitor {
65+
functions: Vec<FunctionInfo>,
66+
}
67+
68+
impl FunctionVisitor {
69+
fn new() -> Self {
70+
Self {
71+
functions: Vec::new(),
72+
}
73+
}
74+
}
75+
76+
impl Visit for FunctionVisitor {
77+
/// Visit function declarations: function foo() {}
78+
fn visit_fn_decl(&mut self, node: &FnDecl) {
79+
self.functions.push(FunctionInfo {
80+
name: node.ident.sym.to_string(),
81+
param_count: node.function.params.len(),
82+
is_exported: false,
83+
});
84+
node.visit_children_with(self);
85+
}
86+
87+
/// Visit function expressions: const foo = function() {}
88+
fn visit_var_declarator(&mut self, node: &VarDeclarator) {
89+
if let swc_ecma_ast::Pat::Ident(ident) = &node.name {
90+
if let Some(init) = &node.init {
91+
match &**init {
92+
swc_ecma_ast::Expr::Fn(fn_expr) => {
93+
self.functions.push(FunctionInfo {
94+
name: ident.id.sym.to_string(),
95+
param_count: fn_expr.function.params.len(),
96+
is_exported: false,
97+
});
98+
}
99+
swc_ecma_ast::Expr::Arrow(arrow_fn) => {
100+
self.functions.push(FunctionInfo {
101+
name: ident.id.sym.to_string(),
102+
param_count: arrow_fn.params.len(),
103+
is_exported: false,
104+
});
105+
}
106+
_ => {}
107+
}
108+
}
109+
}
110+
node.visit_children_with(self);
111+
}
112+
113+
/// Visit export declarations: export function foo() {}
114+
fn visit_export_decl(&mut self, node: &ExportDecl) {
115+
match &node.decl {
116+
Decl::Fn(fn_decl) => {
117+
self.functions.push(FunctionInfo {
118+
name: fn_decl.ident.sym.to_string(),
119+
param_count: fn_decl.function.params.len(),
120+
is_exported: true,
121+
});
122+
}
123+
_ => {}
124+
}
125+
node.visit_children_with(self);
126+
}
127+
128+
/// Visit named exports: export { foo }
129+
fn visit_named_export(&mut self, node: &NamedExport) {
130+
for spec in &node.specifiers {
131+
match spec {
132+
ExportSpecifier::Named(named) => {
133+
let name = match &named.orig {
134+
ModuleExportName::Ident(ident) => ident.sym.to_string(),
135+
ModuleExportName::Str(str_lit) => str_lit.value.to_string(),
136+
};
137+
138+
if let Some(func) = self.functions.iter_mut().find(|f| f.name == name) {
139+
func.is_exported = true;
140+
}
141+
}
142+
_ => {}
143+
}
144+
}
145+
node.visit_children_with(self);
146+
}
147+
}
148+
149+
fn parse_js_file(file_path: &Path) -> Result<Vec<FunctionInfo>> {
150+
let js_content = fs::read_to_string(&file_path).map_err(|e| {
151+
syn::Error::new(
152+
proc_macro2::Span::call_site(),
153+
format!(
154+
"Could not read JavaScript file '{}': {}",
155+
file_path.display(),
156+
e
157+
),
158+
)
159+
})?;
160+
161+
let cm = Arc::new(SourceMap::default());
162+
let fm = cm.new_source_file(
163+
swc_common::FileName::Custom(file_path.display().to_string()).into(),
164+
js_content.clone(),
165+
);
166+
167+
let lexer = Lexer::new(
168+
Syntax::Es(EsSyntax {
169+
jsx: true,
170+
..Default::default()
171+
}),
172+
Default::default(),
173+
StringInput::from(&*fm),
174+
None,
175+
);
176+
177+
let mut parser = Parser::new_from(lexer);
178+
179+
let module = parser.parse_module().map_err(|e| {
180+
syn::Error::new(
181+
proc_macro2::Span::call_site(),
182+
format!(
183+
"Failed to parse JavaScript file '{}': {:?}",
184+
file_path.display(),
185+
e
186+
),
187+
)
188+
})?;
189+
190+
let mut visitor = FunctionVisitor::new();
191+
module.visit_with(&mut visitor);
192+
193+
Ok(visitor.functions)
194+
}
195+
196+
fn validate_function_call(
197+
functions: &[FunctionInfo],
198+
function_name: &str,
199+
arg_count: usize,
200+
) -> Result<()> {
201+
let function = functions
202+
.iter()
203+
.find(|f| f.name == function_name)
204+
.ok_or_else(|| {
205+
syn::Error::new(
206+
proc_macro2::Span::call_site(),
207+
format!("Function '{}' not found in JavaScript file", function_name),
208+
)
209+
})?;
210+
211+
if !function.is_exported {
212+
return Err(syn::Error::new(
213+
proc_macro2::Span::call_site(),
214+
format!(
215+
"Function '{}' is not exported from the JavaScript module",
216+
function_name
217+
),
218+
));
219+
}
220+
221+
if function.param_count != arg_count {
222+
return Err(syn::Error::new(
223+
proc_macro2::Span::call_site(),
224+
format!(
225+
"Function '{}' expects {} arguments, but {} were provided",
226+
function_name, function.param_count, arg_count
227+
),
228+
));
229+
}
230+
231+
Ok(())
232+
}
233+
234+
#[proc_macro]
235+
pub fn call_js(input: TokenStream) -> TokenStream {
236+
// parse
237+
let input = parse_macro_input!(input as CallJsInput);
238+
239+
let asset_path = &input.asset_path;
240+
let function_call = &input.function_call;
241+
242+
let function_name = match extract_function_name(function_call) {
243+
Ok(name) => name,
244+
Err(e) => return TokenStream::from(e.to_compile_error()),
245+
};
246+
247+
// validate js call
248+
let arg_count = function_call.args.len();
249+
let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
250+
Ok(dir) => dir,
251+
Err(_) => {
252+
return TokenStream::from(
253+
syn::Error::new(
254+
proc_macro2::Span::call_site(),
255+
"CARGO_MANIFEST_DIR environment variable not found",
256+
)
257+
.to_compile_error(),
258+
);
259+
}
260+
};
261+
262+
let js_file_path = std::path::Path::new(&manifest_dir).join(asset_path.value());
263+
let functions = match parse_js_file(&js_file_path) {
264+
Ok(funcs) => funcs,
265+
Err(e) => return TokenStream::from(e.to_compile_error()),
266+
};
267+
if let Err(e) = validate_function_call(&functions, &function_name, arg_count) {
268+
return TokenStream::from(e.to_compile_error());
269+
}
270+
271+
// expand
272+
let send_calls: Vec<TokenStream2> = function_call
273+
.args
274+
.iter()
275+
.map(|arg| quote! { eval.send(#arg)?; })
276+
.collect();
277+
278+
let mut js_format = format!(r#"const {{{{ {function_name} }}}} = await import("{{}}");"#,);
279+
for i in 0..arg_count {
280+
js_format.push_str(&format!("\nlet arg{} = await dioxus.recv();", i));
281+
}
282+
js_format.push_str(&format!("\nreturn {}(", function_name));
283+
for i in 0..arg_count {
284+
if i > 0 {
285+
js_format.push_str(", ");
286+
}
287+
js_format.push_str(&format!("arg{}", i));
288+
}
289+
js_format.push_str(");");
290+
291+
let expanded = quote! {
292+
async move {
293+
const MODULE: Asset = asset!(#asset_path);
294+
let js = format!(#js_format, MODULE);
295+
let eval = document::eval(js.as_str());
296+
#(#send_calls)*
297+
eval.await
298+
}
299+
};
300+
301+
TokenStream::from(expanded)
302+
}

0 commit comments

Comments
 (0)