Browse Source

feat(build): support plugins that are defined in app crate (#8781)

* feat(build): support plugins that are defined in app crate

* dx
Lucas Fernandes Nogueira 1 năm trước cách đây
mục cha
commit
edb11c138d

+ 5 - 0
.changes/inline-plugins.md

@@ -0,0 +1,5 @@
+---
+'tauri-build': patch:enhance
+---
+
+Added `Attributes::plugin()` to register a plugin that is inlined in the application crate.

+ 90 - 1
core/tauri-build/src/lib.rs

@@ -24,6 +24,7 @@ use tauri_utils::{
 };
 
 use std::{
+  collections::HashMap,
   env::var_os,
   fs::copy,
   path::{Path, PathBuf},
@@ -331,6 +332,41 @@ impl WindowsAttributes {
   }
 }
 
+/// Definition of a plugin that is part of the Tauri application instead of having its own crate.
+///
+/// By default it generates a plugin manifest that parses permissions from the `permissions/$plugin-name` directory.
+/// To change the glob pattern that is used to find permissions, use [`Self::permissions_path_pattern`].
+///
+/// To autogenerate permissions for each of the plugin commands, see [`Self::commands`].
+#[derive(Debug, Default)]
+pub struct InlinedPlugin {
+  commands: &'static [&'static str],
+  permissions_path_pattern: Option<&'static str>,
+}
+
+impl InlinedPlugin {
+  pub fn new() -> Self {
+    Self::default()
+  }
+
+  /// Define a list of commands that gets permissions autogenerated in the format of `allow-$command` and `deny-$command`
+  /// where $command is the command in kebab-case.
+  pub fn commands(mut self, commands: &'static [&'static str]) -> Self {
+    self.commands = commands;
+    self
+  }
+
+  /// Sets a glob pattern that is used to find the permissions of this inlined plugin.
+  ///
+  /// **Note:** You must emit [rerun-if-changed] instructions for the plugin permissions directory.
+  ///
+  /// By default it is `./permissions/$plugin-name/**/*`
+  pub fn permissions_path_pattern(mut self, pattern: &'static str) -> Self {
+    self.permissions_path_pattern.replace(pattern);
+    self
+  }
+}
+
 /// The attributes used on the build.
 #[derive(Debug, Default)]
 pub struct Attributes {
@@ -339,6 +375,7 @@ pub struct Attributes {
   capabilities_path_pattern: Option<&'static str>,
   #[cfg(feature = "codegen")]
   codegen: Option<codegen::context::CodegenContext>,
+  inlined_plugins: HashMap<&'static str, InlinedPlugin>,
 }
 
 impl Attributes {
@@ -365,6 +402,14 @@ impl Attributes {
     self
   }
 
+  /// Adds the given plugin to the list of inlined plugins (a plugin that is part of your application).
+  ///
+  /// See [`InlinedPlugin`] for more information.
+  pub fn plugin(mut self, name: &'static str, plugin: InlinedPlugin) -> Self {
+    self.inlined_plugins.insert(name, plugin);
+    self
+  }
+
   #[cfg(feature = "codegen")]
   #[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
   #[must_use]
@@ -473,7 +518,51 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
   let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
 
   manifest::check(&config, &mut manifest)?;
-  let plugin_manifests = acl::get_plugin_manifests()?;
+  let mut plugin_manifests = acl::get_plugin_manifests()?;
+  for (name, plugin) in attributes.inlined_plugins {
+    let plugin_out_dir = out_dir.join("plugins").join(name);
+
+    let mut permission_files = if plugin.commands.is_empty() {
+      Vec::new()
+    } else {
+      tauri_utils::acl::build::autogenerate_command_permissions(
+        &plugin_out_dir,
+        plugin.commands,
+        "",
+      );
+      tauri_utils::acl::build::define_permissions(
+        &plugin_out_dir.join("*").to_string_lossy(),
+        name,
+        &plugin_out_dir,
+      )?
+    };
+
+    if let Some(pattern) = plugin.permissions_path_pattern {
+      permission_files.extend(tauri_utils::acl::build::define_permissions(
+        pattern,
+        name,
+        &plugin_out_dir,
+      )?);
+    } else {
+      let default_permissions_path = Path::new("permissions").join(name);
+      println!(
+        "cargo:rerun-if-changed={}",
+        default_permissions_path.display()
+      );
+      permission_files.extend(tauri_utils::acl::build::define_permissions(
+        &default_permissions_path
+          .join("**")
+          .join("*")
+          .to_string_lossy(),
+        name,
+        &plugin_out_dir,
+      )?);
+    }
+
+    let manifest = tauri_utils::acl::plugin::Manifest::new(permission_files, None);
+    plugin_manifests.insert(name.into(), manifest);
+  }
+
   std::fs::write(
     out_dir.join(PLUGIN_MANIFESTS_FILE_NAME),
     serde_json::to_string(&plugin_manifests)?,

+ 5 - 2
examples/api/src-tauri/build.rs

@@ -8,6 +8,9 @@ fn main() {
     codegen = codegen.dev();
   }
 
-  tauri_build::try_build(tauri_build::Attributes::new().codegen(codegen))
-    .expect("failed to run tauri-build");
+  tauri_build::try_build(tauri_build::Attributes::new().codegen(codegen).plugin(
+    "app-menu",
+    tauri_build::InlinedPlugin::new().commands(&["toggle", "popup"]),
+  ))
+  .expect("failed to run tauri-build");
 }

+ 1 - 0
examples/api/src-tauri/capabilities/run-app.json

@@ -4,6 +4,7 @@
   "description": "permissions to run the app",
   "windows": ["main", "main-*"],
   "permissions": [
+    "app-menu:default",
     "sample:allow-ping-scoped",
     "sample:global-scope",
     "path:default",

+ 3 - 0
examples/api/src-tauri/permissions/app-menu/default.toml

@@ -0,0 +1,3 @@
+[default]
+description = "Default permissions for the plugin"
+permissions = ["allow-toggle", "allow-popup"]

+ 0 - 34
examples/api/src-tauri/src/cmd.rs

@@ -29,37 +29,3 @@ pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
     message: "message response".into(),
   }
 }
-
-#[cfg(all(desktop, not(target_os = "macos")))]
-#[command]
-pub fn toggle_menu<R: tauri::Runtime>(window: tauri::Window<R>) {
-  if window.is_menu_visible().unwrap_or_default() {
-    let _ = window.hide_menu();
-  } else {
-    let _ = window.show_menu();
-  }
-}
-
-#[cfg(target_os = "macos")]
-#[command]
-pub fn toggle_menu<R: tauri::Runtime>(
-  app: tauri::AppHandle<R>,
-  app_menu: tauri::State<'_, crate::AppMenu<R>>,
-) {
-  if let Some(menu) = app.remove_menu().unwrap() {
-    app_menu.0.lock().unwrap().replace(menu);
-  } else {
-    app
-      .set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu"))
-      .unwrap();
-  }
-}
-
-#[cfg(desktop)]
-#[command]
-pub fn popup_context_menu<R: tauri::Runtime>(
-  window: tauri::Window<R>,
-  popup_menu: tauri::State<'_, crate::PopupMenu<R>>,
-) {
-  window.popup_menu(&popup_menu.0).unwrap();
-}

+ 3 - 4
examples/api/src-tauri/src/lib.rs

@@ -4,6 +4,8 @@
 
 mod cmd;
 #[cfg(desktop)]
+mod menu_plugin;
+#[cfg(desktop)]
 mod tray;
 
 use serde::Serialize;
@@ -46,6 +48,7 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
         let handle = app.handle();
         tray::create_tray(handle)?;
         handle.plugin(tauri_plugin_cli::init())?;
+        handle.plugin(menu_plugin::init())?;
       }
 
       #[cfg(target_os = "macos")]
@@ -140,10 +143,6 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
     .invoke_handler(tauri::generate_handler![
       cmd::log_operation,
       cmd::perform_request,
-      #[cfg(desktop)]
-      cmd::toggle_menu,
-      #[cfg(desktop)]
-      cmd::popup_context_menu
     ])
     .build(tauri::tauri_build_context!())
     .expect("error while building tauri application");

+ 48 - 0
examples/api/src-tauri/src/menu_plugin.rs

@@ -0,0 +1,48 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use tauri::{
+  command,
+  plugin::{Builder, TauriPlugin},
+  Runtime,
+};
+
+#[cfg(not(target_os = "macos"))]
+#[command]
+pub fn toggle<R: tauri::Runtime>(window: tauri::Window<R>) {
+  if window.is_menu_visible().unwrap_or_default() {
+    let _ = window.hide_menu();
+  } else {
+    let _ = window.show_menu();
+  }
+}
+
+#[cfg(target_os = "macos")]
+#[command]
+pub fn toggle<R: tauri::Runtime>(
+  app: tauri::AppHandle<R>,
+  app_menu: tauri::State<'_, crate::AppMenu<R>>,
+) {
+  if let Some(menu) = app.remove_menu().unwrap() {
+    app_menu.0.lock().unwrap().replace(menu);
+  } else {
+    app
+      .set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu"))
+      .unwrap();
+  }
+}
+
+#[command]
+pub fn popup<R: tauri::Runtime>(
+  window: tauri::Window<R>,
+  popup_menu: tauri::State<'_, crate::PopupMenu<R>>,
+) {
+  window.popup_menu(&popup_menu.0).unwrap();
+}
+
+pub fn init<R: Runtime>() -> TauriPlugin<R> {
+  Builder::new("app-menu")
+    .invoke_handler(tauri::generate_handler![popup, toggle])
+    .build()
+}

+ 14 - 11
examples/api/src/App.svelte

@@ -1,5 +1,5 @@
 <script>
-  import { onMount, tick } from "svelte";
+  import { onMount, tick } from 'svelte'
   import { writable } from 'svelte/store'
   import { invoke } from '@tauri-apps/api/core'
 
@@ -13,7 +13,7 @@
 
   document.addEventListener('keydown', (event) => {
     if (event.ctrlKey && event.key === 'b') {
-      invoke('toggle_menu')
+      invoke('plugin:app-menu|toggle')
     }
   })
 
@@ -81,7 +81,7 @@
 
   // Console
   let messages = writable([])
-  let consoleTextEl;
+  let consoleTextEl
   async function onMessage(value) {
     messages.update((r) => [
       ...r,
@@ -90,10 +90,10 @@
           `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
           (typeof value === 'string' ? value : JSON.stringify(value, null, 1)) +
           '</pre>'
-      },
+      }
     ])
-     await tick();
-    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight;
+    await tick()
+    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
   }
 
   // this function is renders HTML without sanitizing it so it's insecure
@@ -106,10 +106,10 @@
           `<pre><strong class="text-accent dark:text-darkAccent">[${new Date().toLocaleTimeString()}]:</strong> ` +
           html +
           '</pre>'
-      },
+      }
     ])
-    await tick();
-    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight;
+    await tick()
+    if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
   }
 
   function clear() {
@@ -329,13 +329,16 @@
                 hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
                 active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
           "
-          on:keypress={(e) => e.key === "Enter"? clear() : {} }
+          on:keypress={(e) => (e.key === 'Enter' ? clear() : {})}
           on:click={clear}
         >
           <div class="i-codicon-clear-all" />
         </div>
       </div>
-      <div bind:this={consoleTextEl} class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2">
+      <div
+        bind:this={consoleTextEl}
+        class="px-2 overflow-y-auto all:font-mono code-block all:text-xs select-text mr-2"
+      >
         {#each $messages as r}
           {@html r.html}
         {/each}

+ 1 - 1
examples/api/src/views/Welcome.svelte

@@ -17,7 +17,7 @@
   })
 
   function contextMenu() {
-    invoke('popup_context_menu')
+    invoke('plugin:app-menu|popup')
   }
 </script>