diff --git a/Cargo.lock b/Cargo.lock index fe8240b..5d5a441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -143,12 +155,27 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object 0.32.2", +] + [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii" version = "1.1.0" @@ -182,6 +209,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4" +[[package]] +name = "ast_node" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a184645bcc6f52d69d8e7639720699c6a99efb711f886e251ed1d16db8dd90e" +dependencies = [ + "quote", + "swc_macros_common", + "syn 2.0.110", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -418,7 +456,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", "windows-link 0.2.1", ] @@ -478,6 +516,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "better_scoped_tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd228125315b132eed175bf47619ac79b945b26e56b848ba203ae4ea8603609" +dependencies = [ + "scoped-tls", +] + [[package]] name = "bincode" version = "1.3.3" @@ -651,6 +698,9 @@ name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +dependencies = [ + "allocator-api2", +] [[package]] name = "byteorder" @@ -667,6 +717,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-str" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c60b5ce37e0b883c37eb89f79a1e26fbe9c1081945d024eee93e8d91a7e18b3" +dependencies = [ + "bytes", + "serde", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -1167,6 +1227,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "cow-replace" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cab584c4b83b5b36f81a10bd15191fd77f70432d624787ee68ec64edd6d7ed" +dependencies = [ + "ascii", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2218,6 +2287,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "dioxus-use-js-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_visit", + "syn 2.0.110", +] + [[package]] name = "dioxus-web" version = "0.7.1" @@ -2788,6 +2870,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "from_variant" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308530a56b099da144ebc5d8e179f343ad928fa2b3558d1eb3db9af18d6eff43" +dependencies = [ + "swc_macros_common", + "syn 2.0.110", +] + [[package]] name = "futf" version = "0.1.5" @@ -3409,6 +3501,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -3564,6 +3660,19 @@ dependencies = [ "digest", ] +[[package]] +name = "hstr" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85186bc48d3c611ead052cc3f907748e40b63d73a99e4ed34d18063e2baaf1b" +dependencies = [ + "hashbrown 0.14.5", + "new_debug_unreachable", + "once_cell", + "rustc-hash 2.1.1", + "triomphe", +] + [[package]] name = "html5ever" version = "0.27.0" @@ -3982,6 +4091,18 @@ dependencies = [ "serde", ] +[[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -4193,7 +4314,6 @@ dependencies = [ "serde_html_form", "serde_json", "sha2", - "signature", "smol_str", "thiserror 2.0.17", "tokio", @@ -5161,6 +5281,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -5369,6 +5500,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -5619,7 +5759,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_macros", + "phf_macros 0.10.0", "phf_shared 0.10.0", "proc-macro-hack", ] @@ -5630,6 +5770,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -5697,6 +5838,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -5963,6 +6117,16 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "publicsuffix" version = "2.3.0" @@ -6675,6 +6839,12 @@ dependencies = [ "futures-core", ] +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -7047,6 +7217,17 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "smol_str" version = "0.3.4" @@ -7149,6 +7330,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "static-regular-grammar" version = "2.0.2" @@ -7200,6 +7394,17 @@ dependencies = [ "quote", ] +[[package]] +name = "string_enum" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" +dependencies = [ + "quote", + "swc_macros_common", + "syn 2.0.110", +] + [[package]] name = "strsim" version = "0.11.1" @@ -7261,6 +7466,176 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" +[[package]] +name = "swc_allocator" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7eefd2c8b228a8c73056482b2ae4b3a1071fbe07638e3b55ceca8570cc48bb" +dependencies = [ + "allocator-api2", + "bumpalo", + "rustc-hash 2.1.1", +] + +[[package]] +name = "swc_atoms" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fda66fe4175cddc27a79643ded7ce297b6b2bf133f038ec5907fb4673e4b2ba" +dependencies = [ + "hstr", + "once_cell", + "serde", +] + +[[package]] +name = "swc_common" +version = "13.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865f71f363e63306cedec3f3cf1cb9e80acaa9229261ba2569467a19060c7c8" +dependencies = [ + "anyhow", + "ast_node", + "better_scoped_tls", + "bytes-str", + "cfg-if", + "either", + "from_variant", + "new_debug_unreachable", + "num-bigint", + "once_cell", + "rustc-hash 2.1.1", + "serde", + "siphasher 0.3.11", + "swc_allocator", + "swc_atoms", + "swc_eq_ignore_macros", + "swc_visit", + "tracing", + "unicode-width 0.1.14", + "url", +] + +[[package]] +name = "swc_ecma_ast" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ddc264ed13ae03aa30e1c89798502f9ddbe765a4ad695054add1074ffbc5cb" +dependencies = [ + "bitflags 2.10.0", + "is-macro", + "num-bigint", + "once_cell", + "phf 0.11.3", + "rustc-hash 2.1.1", + "scoped-tls", + "string_enum", + "swc_atoms", + "swc_common", + "swc_visit", + "unicode-id-start", +] + +[[package]] +name = "swc_ecma_lexer" +version = "17.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f318a247bc2c4662dc10dccd7a4f64834add015d216e977a58665b0aad9772" +dependencies = [ + "arrayvec", + "ascii", + "bitflags 2.10.0", + "cow-replace", + "either", + "new_debug_unreachable", + "num-bigint", + "num-traits", + "phf 0.11.3", + "rustc-hash 2.1.1", + "seq-macro", + "serde", + "smallvec", + "smartstring", + "stacker", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "tracing", + "typed-arena", +] + +[[package]] +name = "swc_ecma_parser" +version = "17.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff5b5fbe4316b7d1272ac6f204a871f0d5785755d6a14ddf38e7fa8af73be30" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "either", + "new_debug_unreachable", + "num-bigint", + "num-traits", + "phf 0.11.3", + "rustc-hash 2.1.1", + "serde", + "smallvec", + "smartstring", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_lexer", + "tracing", + "typed-arena", +] + +[[package]] +name = "swc_ecma_visit" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad28e3449b376bfe1f2bde28bfcf305961ba23c1e205bedb03a7c108a1d1ff6" +dependencies = [ + "new_debug_unreachable", + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", + "tracing", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "swc_macros_common" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "swc_visit" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fb71484b486c185e34d2172f0eabe7f4722742aad700f426a494bb2de232a2" +dependencies = [ + "either", + "new_debug_unreachable", +] + [[package]] name = "syn" version = "1.0.109" @@ -8057,6 +8432,10 @@ name = "triomphe" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] [[package]] name = "try-lock" @@ -8129,6 +8508,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.19.0" @@ -8152,6 +8537,12 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-id-start" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/crates/use-js-macro/Cargo.toml b/crates/use-js-macro/Cargo.toml new file mode 100644 index 0000000..9b5935a --- /dev/null +++ b/crates/use-js-macro/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "dioxus-use-js-macro" +version = { workspace = true } +authors = ["Dillon Henry McMahon"] +edition = "2021" +description = "javascript use bridge macro for Dioxus" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["web", "desktop", "mobile", "gui", "wasm"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } +swc_ecma_parser = { version = "=17.0.1", default-features = false } +swc_ecma_ast = { version = "=13.0.0", default-features = false } +swc_ecma_visit = { version = "=13.0.0", default-features = false } +swc_common = { version = "=13.0.1", default-features = false } diff --git a/crates/use-js-macro/src/lib.rs b/crates/use-js-macro/src/lib.rs new file mode 100644 index 0000000..353da8f --- /dev/null +++ b/crates/use-js-macro/src/lib.rs @@ -0,0 +1,481 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use std::{fs, path::Path}; +use swc_common::comments::{CommentKind, Comments}; +use swc_common::Spanned; +use swc_common::{comments::SingleThreadedComments, SourceMap, Span}; +use swc_ecma_ast::{ + Decl, ExportDecl, ExportSpecifier, FnDecl, ModuleExportName, NamedExport, Param, Pat, + VarDeclarator, +}; +use swc_ecma_parser::EsSyntax; +use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; +use swc_ecma_visit::{Visit, VisitWith}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Ident, LitStr, Result, Token, +}; + +#[derive(Debug, Clone)] +enum ImportSpec { + /// * + All, + /// {greeting, other_func} + Named(Vec), + /// greeting + Single(Ident), +} + +struct UseJsInput { + asset_path: LitStr, + import_spec: ImportSpec, +} + +impl Parse for UseJsInput { + fn parse(input: ParseStream) -> Result { + let asset_path: LitStr = input.parse()?; + input.parse::()?; + + let import_spec = if input.peek(Token![*]) { + input.parse::()?; + ImportSpec::All + } else if input.peek(syn::token::Brace) { + let content; + syn::braced!(content in input); + let mut functions = Vec::new(); + + loop { + let ident: Ident = content.parse()?; + functions.push(ident); + + if content.peek(Token![,]) { + content.parse::()?; + if content.is_empty() { + break; + } + } else { + break; + } + } + + ImportSpec::Named(functions) + } else { + let ident: Ident = input.parse()?; + ImportSpec::Single(ident) + }; + + Ok(UseJsInput { + asset_path, + import_spec, + }) + } +} + +#[derive(Debug, Clone)] +struct FunctionInfo { + name: String, + /// If specified in the use declaration + name_ident: Option, + params: Vec, + is_exported: bool, + /// The stripped lines + doc_comment: Vec, +} + +struct FunctionVisitor { + functions: Vec, + comments: SingleThreadedComments, +} + +impl FunctionVisitor { + fn new(comments: SingleThreadedComments) -> Self { + Self { + functions: Vec::new(), + comments, + } + } + + fn extract_doc_comment(&self, span: Span) -> Vec { + // Get leading comments for the span + let leading_comment = self.comments.get_leading(span.lo()); + + if let Some(comments) = leading_comment { + let mut doc_lines = Vec::new(); + + for comment in comments.iter() { + let comment_text = &comment.text; + match comment.kind { + // Handle `///`. `//` is already stripped + CommentKind::Line => { + if let Some(content) = comment_text.strip_prefix("/") { + let cleaned = content.trim_start(); + doc_lines.push(cleaned.to_string()); + } + } + // Handle `/*` `*/`. `/*` `*/` is already stripped + CommentKind::Block => { + for line in comment_text.lines() { + if let Some(cleaned) = line.trim_start().strip_prefix("*") { + doc_lines.push(cleaned.to_string()); + } + } + } + }; + } + + doc_lines + } else { + Vec::new() + } + } +} + +fn function_params_to_names(params: &[Param]) -> Vec { + params + .iter() + .enumerate() + .map(|(i, param)| { + if let Some(ident) = param.pat.as_ident() { + ident.id.sym.to_string() + } else { + format!("arg{}", i) + } + }) + .collect() +} + +fn function_pat_to_names(pats: &[Pat]) -> Vec { + pats.iter() + .enumerate() + .map(|(i, pat)| { + if let Some(ident) = pat.as_ident() { + ident.id.sym.to_string() + } else { + format!("arg{}", i) + } + }) + .collect() +} + +impl Visit for FunctionVisitor { + /// Visit function declarations: function foo() {} + fn visit_fn_decl(&mut self, node: &FnDecl) { + let doc_comment = self.extract_doc_comment(node.span()); + + self.functions.push(FunctionInfo { + name: node.ident.sym.to_string(), + name_ident: None, + params: function_params_to_names(&node.function.params), + is_exported: false, + doc_comment, + }); + node.visit_children_with(self); + } + + /// Visit function expressions: const foo = function() {} + fn visit_var_declarator(&mut self, node: &VarDeclarator) { + if let swc_ecma_ast::Pat::Ident(ident) = &node.name { + if let Some(init) = &node.init { + let doc_comment = self.extract_doc_comment(node.span()); + + match &**init { + swc_ecma_ast::Expr::Fn(fn_expr) => { + self.functions.push(FunctionInfo { + name: ident.id.sym.to_string(), + name_ident: None, + params: function_params_to_names(&fn_expr.function.params), + is_exported: false, + doc_comment, + }); + } + swc_ecma_ast::Expr::Arrow(arrow_fn) => { + self.functions.push(FunctionInfo { + name: ident.id.sym.to_string(), + name_ident: None, + params: function_pat_to_names(&arrow_fn.params), + is_exported: false, + doc_comment, + }); + } + _ => {} + } + } + } + node.visit_children_with(self); + } + + /// Visit export declarations: export function foo() {} + fn visit_export_decl(&mut self, node: &ExportDecl) { + if let Decl::Fn(fn_decl) = &node.decl { + let doc_comment = self.extract_doc_comment(node.span()); + + self.functions.push(FunctionInfo { + name: fn_decl.ident.sym.to_string(), + name_ident: None, + params: function_params_to_names(&fn_decl.function.params), + is_exported: true, + doc_comment, + }); + } + node.visit_children_with(self); + } + + /// Visit named exports: export { foo } + fn visit_named_export(&mut self, node: &NamedExport) { + for spec in &node.specifiers { + if let ExportSpecifier::Named(named) = spec { + let name = match &named.orig { + ModuleExportName::Ident(ident) => ident.sym.to_string(), + ModuleExportName::Str(str_lit) => str_lit.value.to_string(), + }; + + if let Some(func) = self.functions.iter_mut().find(|f| f.name == name) { + func.is_exported = true; + } + } + } + node.visit_children_with(self); + } +} + +fn parse_js_file(file_path: &Path) -> Result> { + let js_content = fs::read_to_string(file_path).map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!( + "Could not read JavaScript file '{}': {}", + file_path.display(), + e + ), + ) + })?; + + let cm = SourceMap::default(); + let fm = cm.new_source_file( + swc_common::FileName::Custom(file_path.display().to_string()).into(), + js_content.clone(), + ); + let comments = SingleThreadedComments::default(); + let lexer = Lexer::new( + Syntax::Es(EsSyntax::default()), + Default::default(), + StringInput::from(&*fm), + Some(&comments), + ); + + let mut parser = Parser::new_from(lexer); + + let module = parser.parse_module().map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!( + "Failed to parse JavaScript file '{}': {:?}", + file_path.display(), + e + ), + ) + })?; + + let mut visitor = FunctionVisitor::new(comments); + module.visit_with(&mut visitor); + + // Functions are added twice for some reason + visitor + .functions + .dedup_by(|e1, e2| e1.name.as_str() == e2.name.as_str()); + Ok(visitor.functions) +} + +fn remove_function_info(name: &str, functions: &mut Vec) -> Result { + if let Some(pos) = functions.iter().position(|f| f.name == name) { + Ok(functions.remove(pos)) + } else { + Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!("Function '{}' not found in JavaScript file", name), + )) + } +} + +fn get_functions_to_generate( + mut functions: Vec, + import_spec: ImportSpec, +) -> Result> { + match import_spec { + ImportSpec::All => Ok(functions), + ImportSpec::Single(name) => { + let mut func = remove_function_info(name.to_string().as_str(), &mut functions)?; + func.name_ident.replace(name); + Ok(vec![func]) + } + ImportSpec::Named(names) => { + let mut result = Vec::new(); + for name in names { + let mut func = remove_function_info(name.to_string().as_str(), &mut functions)?; + func.name_ident.replace(name); + result.push(func); + } + Ok(result) + } + } +} + +fn generate_function_wrapper(func: &FunctionInfo, asset_path: &LitStr) -> TokenStream2 { + let send_calls: Vec = func + .params + .iter() + .map(|param| { + let param = format_ident!("{}", param); + quote! { + eval.send(#param)?; + } + }) + .collect(); + + let js_func_name = &func.name; + let mut js_format = format!(r#"const {{{{ {js_func_name} }}}} = await import("{{}}");"#); + for param in func.params.iter() { + js_format.push_str(&format!("\nlet {} = await dioxus.recv();", param)); + } + js_format.push_str(&format!("\nreturn {}(", js_func_name)); + for (i, param) in func.params.iter().enumerate() { + if i > 0 { + js_format.push_str(", "); + } + js_format.push_str(param.as_str()); + } + js_format.push_str(");"); + + let param_types: Vec<_> = func + .params + .iter() + .map(|param| { + let param = format_ident!("{}", param); + quote! { #param: impl serde::Serialize } + }) + .collect(); + + // Generate documentation comment if available - preserve original JSDoc format + let doc_comment = if func.doc_comment.is_empty() { + quote! {} + } else { + let doc_lines: Vec<_> = func + .doc_comment + .iter() + .map(|line| quote! { #[doc = #line] }) + .collect(); + quote! { #(#doc_lines)* } + }; + + let func_name = func + .name_ident + .clone() + // Can not exist if `::*` + .unwrap_or_else(|| Ident::new(func.name.as_str(), proc_macro2::Span::call_site())); + quote! { + #doc_comment + pub async fn #func_name(#(#param_types),*) -> Result { + const MODULE: Asset = asset!(#asset_path); + let js = format!(#js_format, MODULE); + let eval = document::eval(js.as_str()); + #(#send_calls)* + eval.await + } + } +} + +/// A macro to create rust binding to javascript functions. +///```rust,ignore +/// use dioxus::prelude::*; +/// +/// // Generate the greeting function at compile time +/// use_js!("examples/assets/example.js"::greeting); +/// +/// // Or generate multiple functions: +/// // use_js!("examples/assets/example.js"::{greeting, add}); +/// +/// // Or generate all exported functions: +/// // use_js!("examples/assets/example.js"::*); +/// +/// fn main() { +/// launch(App); +/// } +/// +/// #[component] +/// fn App() -> Element { +/// let future = use_resource(|| async move { +/// let from = "dave"; +/// let to = "john"; +/// +/// // Now we can call the generated function directly! +/// let greeting_result = greeting(from, to) +/// .await +/// .map_err(Box::::from)?; +/// let greeting: String = +/// serde_json::from_value(greeting_result).map_err(Box::::from)?; +/// Ok::>(greeting) +/// }); +/// +/// rsx!( +/// div { +/// h1 { "Dioxus `use_js!` macro example!" } +/// { +/// match &*future.read() { +/// Some(Ok(greeting)) => rsx! { +/// p { "Greeting from JavaScript: {greeting}" } +/// }, +/// Some(Err(e)) => rsx! { +/// p { "Error: {e}" } +/// }, +/// None => rsx! { +/// p { "Running js..." } +/// }, +/// } +/// } +/// } +/// ) +/// } +/// ``` +#[proc_macro] +pub fn use_js(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as UseJsInput); + + let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") { + Ok(dir) => dir, + Err(_) => { + return TokenStream::from( + syn::Error::new( + proc_macro2::Span::call_site(), + "CARGO_MANIFEST_DIR environment variable not found", + ) + .to_compile_error(), + ); + } + }; + + let asset_path = &input.asset_path; + let js_file_path = std::path::Path::new(&manifest_dir).join(asset_path.value()); + + let all_functions = match parse_js_file(&js_file_path) { + Ok(funcs) => funcs, + Err(e) => return TokenStream::from(e.to_compile_error()), + }; + + let import_spec = input.import_spec; + let functions_to_generate = match get_functions_to_generate(all_functions, import_spec) { + Ok(funcs) => funcs, + Err(e) => return TokenStream::from(e.to_compile_error()), + }; + + let function_wrappers: Vec = functions_to_generate + .iter() + .map(|func| generate_function_wrapper(func, asset_path)) + .collect(); + + let expanded = quote! { + #(#function_wrappers)* + }; + + TokenStream::from(expanded) +} diff --git a/crates/weaver-app/Cargo.toml b/crates/weaver-app/Cargo.toml index 3215195..a866830 100644 --- a/crates/weaver-app/Cargo.toml +++ b/crates/weaver-app/Cargo.toml @@ -21,7 +21,6 @@ server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum"] dashmap = "6.1.0" dioxus = { version = "0.7.1", features = ["router"] } -#dioxus-router = { version = "0.7.1", features = ["wasm-split"] } weaver-common = { path = "../weaver-common" } jacquard = { workspace = true}#, features = ["streaming"] } jacquard-lexicon = { workspace = true } @@ -40,7 +39,8 @@ serde_json = "1.0" humansize = "2.0.0" base64 = "0.22" http = "1.3" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["json"] } + dioxus-free-icons = { version = "0.10.0" } # diesel = { version = "2.3", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "chrono", "serde_json"] } # diesel_migrations = { version = "2.3", features = ["sqlite"] } @@ -50,8 +50,10 @@ tracing.workspace = true [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] webbrowser = "1.0.6" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +reqwest = { version = "0.12", default-features = false, features = ["json"] } #sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] } time = { version = "0.3", features = ["wasm-bindgen"] } console_error_panic_hook = "0.1" diff --git a/crates/weaver-app/src/auth/mod.rs b/crates/weaver-app/src/auth/mod.rs index 9ebdab7..67ba1b7 100644 --- a/crates/weaver-app/src/auth/mod.rs +++ b/crates/weaver-app/src/auth/mod.rs @@ -11,7 +11,7 @@ use dioxus::prelude::*; use jacquard::oauth::types::OAuthClientMetadata; #[cfg(all(feature = "fullstack-server", feature = "server"))] -#[get("/client-metadata.json")] +#[get("/oauth-client-metadata.json")] pub async fn client_metadata() -> Result> { use jacquard::oauth::atproto::atproto_client_metadata; diff --git a/crates/weaver-app/src/config.rs b/crates/weaver-app/src/config.rs index b5b1a34..f355ee7 100644 --- a/crates/weaver-app/src/config.rs +++ b/crates/weaver-app/src/config.rs @@ -130,7 +130,7 @@ impl OAuthConfig { ) } else { let host = env::WEAVER_APP_HOST; - let client_id = format!("{host}/client-metadata.json"); + let client_id = format!("{host}/oauth-client-metadata.json"); let redirect_uri = format!("{host}/callback"); let logo_uri = if env::WEAVER_LOGO_URI.is_empty() { None diff --git a/crates/weaver-app/src/fetch.rs b/crates/weaver-app/src/fetch.rs index b3c93f4..8731a59 100644 --- a/crates/weaver-app/src/fetch.rs +++ b/crates/weaver-app/src/fetch.rs @@ -84,7 +84,7 @@ impl HttpClient for Client { impl XrpcClient for Client { #[doc = " Get the base URI for the client."] - fn base_uri(&self) -> impl Future + Send { + fn base_uri(&self) -> impl Future> + Send { async { let guard = self.session.read().await; if let Some(session) = guard.clone() { @@ -294,7 +294,7 @@ impl AgentSession for Client { } #[doc = " Current base endpoint."] - async fn endpoint(&self) -> jacquard::url::Url { + async fn endpoint(&self) -> CowStr<'static> { let guard = self.session.read().await; if let Some(session) = guard.clone() { session.endpoint().await @@ -647,7 +647,7 @@ impl HttpClient for CachedFetcher { impl XrpcClient for CachedFetcher { #[doc = " Get the base URI for the client."] - fn base_uri(&self) -> impl Future + Send { + fn base_uri(&self) -> impl Future> + Send { self.client.base_uri() } @@ -795,7 +795,7 @@ impl AgentSession for CachedFetcher { self.client.session_info().await } - async fn endpoint(&self) -> jacquard::url::Url { + async fn endpoint(&self) -> CowStr<'static> { self.client.endpoint().await } diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index e0ba79d..f876c8d 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -129,19 +129,38 @@ fn main() { let blob_cache = Arc::new(BlobCache::new(Arc::new( UnauthenticatedSession::new_public(), ))); - dioxus::server::router(App).layer(middleware::from_fn({ - let blob_cache = blob_cache.clone(); - let fetcher = fetcher.clone(); - move |mut req: Request, next: Next| { + axum::Router::new() + // Server side render the application, serve static assets, and register server functions + .serve_dioxus_application( + ServeConfig::builder() + // Enable incremental rendering + .incremental( + dioxus::server::IncrementalRendererConfig::new() + .static_dir( + std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("public"), + ) + .clear_cache(false), + ) + .enable_out_of_order_streaming(), + App, + ) + .layer(middleware::from_fn({ let blob_cache = blob_cache.clone(); let fetcher = fetcher.clone(); - async move { - req.extensions_mut().insert(blob_cache); - req.extensions_mut().insert(fetcher); - Ok::<_, Infallible>(next.run(req).await) + move |mut req: Request, next: Next| { + let blob_cache = blob_cache.clone(); + let fetcher = fetcher.clone(); + async move { + req.extensions_mut().insert(blob_cache); + req.extensions_mut().insert(fetcher); + Ok::<_, Infallible>(next.run(req).await) + } } - } - })) + })) }; Ok(router) }); @@ -240,3 +259,12 @@ pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result Result, ServerFnError> { + // The `Routable` trait has a `static_routes` method that returns all static routes in the enum + Ok(Route::static_routes() + .iter() + .map(ToString::to_string) + .collect()) +}