diff --git a/.changes/add-macros-allow-rename-command.md b/.changes/add-macros-allow-rename-command.md new file mode 100644 index 000000000000..d5d316126fcb --- /dev/null +++ b/.changes/add-macros-allow-rename-command.md @@ -0,0 +1,5 @@ +--- +"tauri-macros": minor:feat +--- + +Add support for rename command macro in tauri-macros \ No newline at end of file diff --git a/crates/tauri-macros/src/command/handler.rs b/crates/tauri-macros/src/command/handler.rs index add699b73da2..e286e02b5c02 100644 --- a/crates/tauri-macros/src/command/handler.rs +++ b/crates/tauri-macros/src/command/handler.rs @@ -151,14 +151,28 @@ impl From for proc_macro::TokenStream { ) -> Self { let cmd = format_ident!("__tauri_cmd__"); let invoke = format_ident!("__tauri_invoke__"); - let (paths, attrs): (Vec, Vec>) = command_defs - .into_iter() - .map(|def| (def.path, def.attrs)) - .unzip(); + let mut paths: Vec = Vec::new(); + let mut attrs: Vec> = Vec::new(); + let mut command_name_consts: Vec = Vec::new(); + for (def, command) in command_defs.into_iter().zip(commands) { + let path = def.path; + let attrs_vec = def.attrs; + let mut const_path = path.clone(); + let last = const_path + .segments + .last_mut() + .expect("path has at least one segment"); + let upper = command.to_string().to_uppercase(); + last.ident = format_ident!("__TAURI_COMMAND_NAME_{}", upper); + paths.push(path); + attrs.push(attrs_vec); + command_name_consts.push(const_path); + } + quote::quote!(move |#invoke| { let #cmd = #invoke.message.command(); match #cmd { - #(#(#attrs)* stringify!(#commands) => #wrappers!(#paths, #invoke),)* + #(#(#attrs)* #command_name_consts => #wrappers!(#paths, #invoke),)* _ => { return false; }, diff --git a/crates/tauri-macros/src/command/wrapper.rs b/crates/tauri-macros/src/command/wrapper.rs index 261257d639c7..18856f0a86c0 100644 --- a/crates/tauri-macros/src/command/wrapper.rs +++ b/crates/tauri-macros/src/command/wrapper.rs @@ -40,6 +40,7 @@ struct WrapperAttributes { root: TokenStream2, execution_context: ExecutionContext, argument_case: ArgumentCase, + rename: RenamePolicy, } impl Parse for WrapperAttributes { @@ -48,6 +49,7 @@ impl Parse for WrapperAttributes { root: quote!(::tauri), execution_context: ExecutionContext::Blocking, argument_case: ArgumentCase::Camel, + rename: RenamePolicy::Keep, }; let attrs = Punctuated::::parse_terminated(input)?; @@ -74,6 +76,19 @@ impl Parse for WrapperAttributes { } }; } + } else if v.path.is_ident("rename") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = v.value + { + let lit = s.value(); + wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit)); + } else { + return Err(syn::Error::new( + v.span(), + "expected string literal for rename", + )); + } } else if v.path.is_ident("root") { if let Expr::Lit(ExprLit { lit: Lit::Str(s), @@ -94,7 +109,7 @@ impl Parse for WrapperAttributes { WrapperAttributeKind::Meta(Meta::Path(_)) => { return Err(syn::Error::new( input.span(), - "unexpected input, expected one of `rename_all`, `root`, `async`", + "unexpected input, expected one of `rename_all`, `rename`, `root`, `async`", )); } WrapperAttributeKind::Async => { @@ -120,6 +135,12 @@ enum ArgumentCase { Camel, } +/// The rename policy for the command. +enum RenamePolicy { + Keep, + Rename(TokenStream2), +} + /// The bindings we attach to `tauri::Invoke`. struct Invoke { message: Ident, @@ -138,9 +159,13 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { attrs.execution_context = ExecutionContext::Async; } - // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` - let maybe_macro_export = match &function.vis { - Visibility::Public(_) | Visibility::Restricted(_) => quote!(#[macro_export]), + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`. + // To avoid crate-root name collisions for same-named commands across modules, + // only export non-renamed commands at crate root. Renamed commands remain module-scoped. + let maybe_macro_export = match (&attrs.rename, &function.vis) { + (RenamePolicy::Keep, Visibility::Public(_) | Visibility::Restricted(_)) => { + quote!(#[macro_export]) + } _ => TokenStream2::default(), }; @@ -270,6 +295,28 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { TokenStream2::default() }; + // For renamed commands (no crate-root export), restrict rename visibility to crate-only + // to avoid public re-export errors for non-exported macros. + let rename_visibility = if let RenamePolicy::Rename(_) = &attrs.rename { + quote!(pub(crate)) + } else { + quote!(#visibility) + }; + + // Always define a hidden constant holding the externally invoked command name. + // This lets the handler match on the renamed string while the original function + // identifier remains usable in `generate_handler![original_fn_name]`. + let command_name_const_ident = { + let upper = function.sig.ident.to_string().to_uppercase(); + format_ident!("__TAURI_COMMAND_NAME_{}", upper) + }; + let command_name_const_value = if let RenamePolicy::Rename(ref rename) = attrs.rename { + quote!(#rename) + } else { + let ident = &function.sig.ident; + quote!(stringify!(#ident)) + }; + // Rely on rust 2018 edition to allow importing a macro from a path. quote!( #async_command_check @@ -277,6 +324,11 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { #maybe_allow_unused #function + // Command name constant used by the handler for pattern matching. + #[doc(hidden)] + #maybe_allow_unused + pub const #command_name_const_ident: &str = #command_name_const_value; + #maybe_allow_unused #maybe_macro_export #[doc(hidden)] @@ -303,7 +355,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { // allow the macro to be resolved with the same path as the command function #[allow(unused_imports)] - #visibility use #wrapper; + #rename_visibility use #wrapper; ) .into() } @@ -467,11 +519,16 @@ fn parse_arg( } let root = &attributes.root; + let command_name = if let RenamePolicy::Rename(r) = &attributes.rename { + quote!(stringify!(#r)) + } else { + quote!(stringify!(#command)) + }; Ok(quote!(#root::ipc::CommandArg::from_command( #root::ipc::CommandItem { plugin: #plugin_name, - name: stringify!(#command), + name: #command_name, key: #key, message: &#message, acl: &#acl,