use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::visit_mut::VisitMut;
use syn::{ImplItem, ImplItemFn, Item, ItemEnum, ItemImpl, ItemStruct, parse_macro_input};
#[proc_macro_attribute]
pub fn js_export(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr: TokenStream2 = attr.into();
let item = parse_macro_input!(item as Item);
let output = match item {
Item::Struct(s) => handle_struct(&attr, s),
Item::Enum(e) => handle_enum(&attr, e),
Item::Impl(i) => handle_impl(&attr, i),
other => {
return syn::Error::new_spanned(other, "#[js_export] only supports struct, enum, impl")
.to_compile_error()
.into();
},
};
output.into()
}
fn handle_struct(attr: &TokenStream2, item: ItemStruct) -> TokenStream2 {
let wasm = wasm_attr(attr);
let napi = napi_attr(attr);
quote! {
#wasm
#napi
#item
}
}
fn handle_enum(attr: &TokenStream2, item: ItemEnum) -> TokenStream2 {
let wasm = wasm_attr(attr);
let napi = napi_attr(attr);
quote! {
#wasm
#napi
#item
}
}
struct JsU64Method {
method: ImplItemFn,
export_args: TokenStream2,
}
fn handle_impl(outer_attr: &TokenStream2, mut item: ItemImpl) -> TokenStream2 {
let self_ty = &item.self_ty;
let generics = &item.generics;
let mut shared_methods: Vec<ImplItemFn> = Vec::new();
let mut jsu64_methods: Vec<JsU64Method> = Vec::new();
let mut other_items: Vec<ImplItem> = Vec::new();
for member in item.items.drain(..) {
match member {
ImplItem::Fn(mut method) => {
let method_attr = extract_js_export_attr(&mut method);
let method_attr_tokens = method_attr.unwrap_or_default();
if has_jsu64(&method) {
jsu64_methods.push(JsU64Method { method, export_args: method_attr_tokens });
} else {
let wasm = wasm_attr(&method_attr_tokens);
let napi = napi_attr(&method_attr_tokens);
method.attrs.push(syn::parse_quote!(#wasm));
method.attrs.push(syn::parse_quote!(#napi));
shared_methods.push(method);
}
},
other => other_items.push(other),
}
}
let mut output = TokenStream2::new();
if !shared_methods.is_empty() || !other_items.is_empty() {
let wasm_outer = wasm_attr(outer_attr);
let napi_outer = napi_attr(outer_attr);
output.extend(quote! {
#wasm_outer
#napi_outer
impl #generics #self_ty {
#(#other_items)*
#(#shared_methods)*
}
});
}
if !jsu64_methods.is_empty() {
let browser_methods: Vec<ImplItemFn> = jsu64_methods
.iter()
.map(|m| make_platform_method(&m.method, &m.export_args, Platform::Browser))
.collect();
let nodejs_methods: Vec<ImplItemFn> = jsu64_methods
.iter()
.map(|m| make_platform_method(&m.method, &m.export_args, Platform::Nodejs))
.collect();
output.extend(quote! {
#[cfg(feature = "browser")]
#[::wasm_bindgen::prelude::wasm_bindgen]
impl #generics #self_ty {
#(#browser_methods)*
}
#[cfg(feature = "nodejs")]
#[::napi_derive::napi]
impl #generics #self_ty {
#(#nodejs_methods)*
}
});
}
output
}
#[derive(Clone, Copy)]
enum Platform {
Browser,
Nodejs,
}
fn make_platform_method(
method: &ImplItemFn,
args: &TokenStream2,
platform: Platform,
) -> ImplItemFn {
let mut method = method.clone();
match platform {
Platform::Browser => {
if args.is_empty() {
method.attrs.push(syn::parse_quote!(#[::wasm_bindgen::prelude::wasm_bindgen]));
} else {
method
.attrs
.push(syn::parse_quote!(#[::wasm_bindgen::prelude::wasm_bindgen(#args)]));
}
},
Platform::Nodejs => {
if args.is_empty() {
method.attrs.push(syn::parse_quote!(#[::napi_derive::napi]));
} else {
method.attrs.push(syn::parse_quote!(#[::napi_derive::napi(#args)]));
}
},
}
let replacement: syn::Path = match platform {
Platform::Browser => syn::parse_quote!(u64),
Platform::Nodejs => syn::parse_quote!(::napi::bindgen_prelude::BigInt),
};
let mut replacer = JsU64Replacer { replacement };
replacer.visit_impl_item_fn_mut(&mut method);
method
}
fn has_jsu64(method: &ImplItemFn) -> bool {
let mut detector = JsU64Detector { found: false };
for arg in &method.sig.inputs {
if let syn::FnArg::Typed(pat_type) = arg {
let mut ty = (*pat_type.ty).clone();
detector.visit_type_mut(&mut ty);
}
}
if let syn::ReturnType::Type(_, ty) = &method.sig.output {
let mut ty = (**ty).clone();
detector.visit_type_mut(&mut ty);
}
detector.found
}
struct JsU64Detector {
found: bool,
}
impl VisitMut for JsU64Detector {
fn visit_type_path_mut(&mut self, tp: &mut syn::TypePath) {
if tp.path.is_ident("JsU64") {
self.found = true;
}
syn::visit_mut::visit_type_path_mut(self, tp);
}
}
struct JsU64Replacer {
replacement: syn::Path,
}
impl VisitMut for JsU64Replacer {
fn visit_impl_item_fn_mut(&mut self, method: &mut ImplItemFn) {
for arg in &mut method.sig.inputs {
self.visit_fn_arg_mut(arg);
}
self.visit_return_type_mut(&mut method.sig.output);
}
fn visit_type_path_mut(&mut self, tp: &mut syn::TypePath) {
if tp.path.is_ident("JsU64") {
tp.path = self.replacement.clone();
}
syn::visit_mut::visit_type_path_mut(self, tp);
}
}
fn wasm_attr(args: &TokenStream2) -> TokenStream2 {
if args.is_empty() {
quote! { #[cfg_attr(feature = "browser", ::wasm_bindgen::prelude::wasm_bindgen)] }
} else {
quote! { #[cfg_attr(feature = "browser", ::wasm_bindgen::prelude::wasm_bindgen(#args))] }
}
}
fn napi_attr(args: &TokenStream2) -> TokenStream2 {
if args.is_empty() {
quote! { #[cfg_attr(feature = "nodejs", ::napi_derive::napi)] }
} else {
quote! { #[cfg_attr(feature = "nodejs", ::napi_derive::napi(#args))] }
}
}
fn extract_js_export_attr(method: &mut ImplItemFn) -> Option<TokenStream2> {
let mut extracted = None;
method.attrs.retain(|attr| {
if attr.path().is_ident("js_export") {
extracted = Some(match &attr.meta {
syn::Meta::Path(_) => TokenStream2::new(),
syn::Meta::List(list) => list.tokens.clone(),
syn::Meta::NameValue(_) => TokenStream2::new(),
});
false
} else {
true
}
});
extracted
}