Saltar al contenido

Desarrollo de Plugins

Los plugins pueden vincularse al ciclo de vida de Tauri, exponer código Rust que se apoya en las APIs de la web view, manejar comandos con código Rust, Kotlin o Swift, y mucho más.

Tauri ofrece un sistema de ventanas con funcionalidad de web view, una forma de enviar mensajes entre el proceso de Rust y la web view, y un sistema de eventos junto con varias herramientas para mejorar la experiencia de desarrollo. Por diseño, el núcleo de Tauri no contiene funciones que no sean necesarias para todos. En su lugar, ofrece un mecanismo para agregar funcionalidades externas a una aplicación Tauri llamado plugins.

Un plugin de Tauri está compuesto por un crate de Cargo y un paquete NPM opcional que proporciona enlaces de API (bindings) para sus comandos y eventos. Además, un proyecto de plugin puede incluir un proyecto de biblioteca de Android y un paquete Swift para iOS. Puedes obtener más información sobre el desarrollo de plugins para Android e iOS en la Guía de desarrollo de plugins móviles.

Los plugins de Tauri tienen un prefijo seguido por el nombre del plugin. El nombre del plugin se especifica en la configuración del plugin bajo tauri.conf.json > plugins.

Por defecto, Tauri añade el prefijo tauri-plugin- a tu crate de plugin. Esto ayuda a que tu plugin sea descubierto por la comunidad de Tauri y a que se use con la CLI de Tauri. Al inicializar un nuevo proyecto de plugin, debes proporcionar su nombre. El nombre del crate generado será tauri-plugin-{plugin-name} y el nombre del paquete NPM JavaScript será tauri-plugin-{plugin-name}-api (aunque recomendamos usar un scope de NPM si es posible). La convención de nombres de Tauri para paquetes NPM es @scope-name/plugin-{plugin-name}.

Para inicializar un nuevo proyecto de plugin, ejecuta plugin new. Si no necesitas el paquete NPM, usa el flag de la CLI --no-api. Si quieres inicializar el plugin con soporte para Android y/o iOS, usa los flags --android y/o --ios.

Después de instalar, puedes ejecutar lo siguiente para crear un proyecto de plugin:

npx @tauri-apps/cli plugin new [name]

Esto inicializará el plugin en el directorio tauri-plugin-[name] y, dependiendo de los flags de la CLI utilizados, el proyecto resultante se verá así:

. tauri-plugin-[name]/
├── src/ - Rust code
│ ├── commands.rs - Defines the commands the webview can use
| ├── desktop.rs - Desktop implementation
| ├── error.rs - Default error type to use in returned results
│ ├── lib.rs - Re-exports appropriate implementation, setup state...
│ ├── mobile.rs - Mobile implementation
│ └── models.rs - Shared structs
├── permissions/ - This will host (generated) permission files for commands
├── android - Android library
├── ios - Swift package
├── guest-js - Source code of the JavaScript API bindings
├── dist-js - Transpiled assets from guest-js
├── Cargo.toml - Cargo crate metadata
└── package.json - NPM package metadata

Si tienes un plugin existente y deseas agregarle capacidades para Android o iOS, puedes usar plugin android add y plugin ios add para inicializar los proyectos de biblioteca móvil y guiarte a través de los cambios necesarios.

Los plugins pueden ejecutar código móvil nativo escrito en Kotlin (o Java) y Swift. La plantilla de plugin por defecto incluye un proyecto de biblioteca Android que utiliza Kotlin y un paquete Swift. Incluye un comando móvil de ejemplo que muestra cómo activar su ejecución desde código Rust.

Lee más sobre el desarrollo de plugins para móviles en la Guía de desarrollo de plugins móviles.

En la aplicación Tauri donde se utiliza el plugin, la configuración del plugin se especifica en tauri.conf.json donde plugin-name es el nombre del plugin:

{
"build": { ... },
"tauri": { ... },
"plugins": {
"plugin-name": {
"timeout": 30
}
}
}

La configuración del plugin se establece en el Builder y se analiza en tiempo de ejecución. Aquí hay un ejemplo de la estructura Config utilizándose para especificar la configuración del plugin:

src/lib.rs
use serde::Deserialize;
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
// Define the plugin config
#[derive(Deserialize)]
pub struct Config {
timeout: usize,
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
// Make the plugin config optional
// by using `Builder::<R, Option<Config>>` instead
Builder::<R, Config>::new("<plugin-name>")
.setup(|app, api| {
let timeout = api.config().timeout;
Ok(())
})
.build()
}

Los plugins pueden vincularse a varios eventos del ciclo de vida:

  • setup: El plugin se está inicializando
  • on_navigation: La web view está intentando realizar una navegación
  • on_webview_ready: Se está creando una nueva ventana
  • on_event: Eventos del bucle de eventos (event loop)
  • on_drop: El plugin se está desconstruyendo

Existen eventos del ciclo de vida adicionales para plugins móviles.

  • Cuándo: El plugin se está inicializando
  • Por qué: Registrar plugins móviles, gestionar estado, ejecutar tareas en segundo plano
src/lib.rs
use tauri::{Manager, plugin::Builder};
use std::{collections::HashMap, sync::Mutex, time::Duration};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, api| {
app.manage(DummyStore(Default::default()));
let app_ = app.clone();
std::thread::spawn(move || {
loop {
app_.emit("tick", ());
std::thread::sleep(Duration::from_secs(1));
}
});
Ok(())
})
  • Cuándo: La web view está intentando realizar una navegación
  • Por qué: Validar la navegación o rastrear cambios de URL

Retornar false cancela la navegación.

src/lib.rs
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_navigation(|window, url| {
println!("window {} is navigating to {}", window.label(), url);
// Cancels the navigation if forbidden
url.scheme() != "forbidden"
})
  • Cuándo: Se ha creado una nueva ventana
  • Por qué: Ejecutar un script de inicialización para cada ventana
src/lib.rs
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_webview_ready(|window| {
window.listen("content-loaded", |event| {
println!("webview content has been loaded");
});
})
  • Cuándo: Eventos del bucle de eventos (event loop)
  • Por qué: Manejar eventos principales como eventos de ventana, eventos de menú y solicitud de salida de la aplicación

Con este hook de ciclo de vida puedes ser notificado de cualquier evento del bucle de eventos.

src/lib.rs
use std::{collections::HashMap, fs::write, sync::Mutex};
use tauri::{plugin::Builder, Manager, RunEvent};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>")
.setup(|app, _api| {
app.manage(DummyStore(Default::default()));
Ok(())
})
.on_event(|app, event| {
match event {
RunEvent::ExitRequested { api, .. } => {
// user requested a window to be closed and there's no windows left
// we can prevent the app from exiting:
api.prevent_exit();
}
RunEvent::Exit => {
// app is going to exit, you can cleanup here
let store = app.state::<DummyStore>();
write(
app.path().app_local_data_dir().unwrap().join("store.json"),
serde_json::to_string(&*store.0.lock().unwrap()).unwrap(),
)
.unwrap();
}
_ => {}
}
})
  • Cuándo: El plugin se está desconstruyendo
  • Por qué: Ejecutar código cuando el plugin ha sido destruido

Consulta Drop para obtener más información.

src/lib.rs
use tauri::plugin::Builder;
Builder::new("<plugin-name>")
.on_drop(|app| {
// plugin has been destroyed...
})

Las APIs del plugin definidas en desktop.rs y mobile.rs del proyecto se exportan al usuario como una estructura (struct) con el mismo nombre del plugin (en PascalCase). Cuando se configura el plugin, se crea una instancia de esta estructura y se gestiona como un estado para que los usuarios puedan recuperarla en cualquier momento con una instancia de Manager (como AppHandle, App o Window) a través del trait de extensión definido en el plugin.

Por ejemplo, el plugin global-shortcut define una estructura GlobalShortcut que se puede leer utilizando el método global_shortcut del trait GlobalShortcutExt:

src-tauri/src/lib.rs
use tauri_plugin_global_shortcut::GlobalShortcutExt;
tauri::Builder::default()
.plugin(tauri_plugin_global_shortcut::init())
.setup(|app| {
app.global_shortcut().register(...);
Ok(())
})

Los comandos se definen en el archivo commands.rs. Son comandos regulares de aplicaciones Tauri. Pueden acceder a las instancias de AppHandle y Window directamente, acceder al estado y tomar entradas de la misma manera que los comandos de la aplicación. Lee la Guía de Comandos para obtener más detalles sobre los comandos de Tauri.

Este comando muestra cómo obtener acceso a la instancia de AppHandle y Window a través de inyección de dependencias, y toma dos parámetros de entrada (on_progress y url):

src/commands.rs
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]
async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) {
// implement command logic here
on_progress.send(100).unwrap();
}

Para exponer el comando a la webview, debes vincularlo a la llamada invoke_handler() en lib.rs:

src/lib.rs
Builder::new("<plugin-name>")
.invoke_handler(tauri::generate_handler![commands::upload])

Define una función de binding en webview-src/index.ts para que los usuarios del plugin puedan llamar fácilmente al comando en JavaScript:

import { invoke, Channel } from '@tauri-apps/api/core'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> {
const onProgress = new Channel<number>()
onProgress.onmessage = onProgressHandler
await invoke('plugin:<plugin-name>|upload', { url, onProgress })
}

Asegúrate de compilar el código TypeScript antes de probarlo.

Por defecto, tus comandos no son accesibles desde el frontend. Si intentas ejecutar uno de ellos, recibirás un rechazo por error de denegación. Para exponer realmente los comandos, también necesitas definir permisos que permitan cada comando.

Los permisos se definen como archivos JSON o TOML dentro del directorio permissions. Cada archivo puede definir una lista de permisos, una lista de conjuntos de permisos y el permiso por defecto de tu plugin.

Un permiso describe los privilegios de los comandos de tu plugin. Puede permitir o denegar una lista de comandos y asociar scopes específicos de comandos y globales.

permissions/start-server.toml
"$schema" = "schemas/schema.json"
[[permission]]
identifier = "allow-start-server"
description = "Enables the start_server command."
commands.allow = ["start_server"]
[[permission]]
identifier = "deny-start-server"
description = "Denies the start_server command."
commands.deny = ["start_server"]

Los scopes permiten que tu plugin defina restricciones más profundas a comandos individuales. Cada permiso puede definir una lista de objetos de scope que definen algo que se permitirá o denegará, ya sea específico para un comando o de forma global para el plugin.

Definamos una estructura de ejemplo que mantendrá los datos de scope para una lista de binarios que un plugin de shell tiene permitido ejecutar:

src/scope.rs
#[derive(Debug, schemars::JsonSchema)]
pub struct Entry {
pub binary: String,
}

El consumidor de tu plugin puede definir un scope para un comando específico en su archivo de capacidades (consulta la documentación). Puedes leer el scope específico del comando con la estructura tauri::ipc::CommandScope:

src/commands.rs
use tauri::ipc::CommandScope;
use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, command_scope: CommandScope<'_, Entry>) -> Result<()> {
let allowed = command_scope.allows();
let denied = command_scope.denies();
todo!()
}

Cuando un permiso no define ningún comando para permitir o denegar, se considera un permiso de scope y solo debe definir un scope global para tu plugin:

permissions/spawn-node.toml
[[permission]]
identifier = "allow-spawn-node"
description = "This scope permits spawning the `node` binary."
[[permission.scope.allow]]
binary = "node"

Puedes leer el scope global con la estructura tauri::ipc::GlobalScope:

src/commands.rs
use tauri::ipc::GlobalScope;
use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, scope: GlobalScope<'_, Entry>) -> Result<()> {
let allowed = scope.allows();
let denied = scope.denies();
todo!()
}

La entrada de scope requiere la dependencia schemars para generar un esquema JSON de modo que los consumidores del plugin conozcan el formato del scope y tengan autocompletado en sus IDEs.

Para definir el esquema, primero agrega la dependencia a tu archivo Cargo.toml:

# we need to add schemars to both dependencies and build-dependencies because the scope.rs module is shared between the app code and build script
[dependencies]
schemars = "0.8"
[build-dependencies]
schemars = "0.8"

En tu script de compilación, agrega el siguiente código:

build.rs
#[path = "src/scope.rs"]
mod scope;
const COMMANDS: &[&str] = &[];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.global_scope_schema(schemars::schema_for!(scope::Entry))
.build();
}

Los conjuntos de permisos son grupos de permisos individuales que ayudan a los usuarios a gestionar tu plugin con un mayor nivel de abstracción. Por ejemplo, si una sola API utiliza múltiples comandos o si hay una conexión lógica entre una colección de comandos, deberías definir un conjunto que los contenga:

permissions/websocket.toml
"$schema" = "schemas/schema.json"
[[set]]
identifier = "allow-websocket"
description = "Allows connecting and sending messages through a WebSocket"
permissions = ["allow-connect", "allow-send"]

El permiso por defecto es un conjunto de permisos especial con el identificador default. Se recomienda habilitar los comandos requeridos por defecto. Por ejemplo, el plugin http no sirve de nada sin el comando request permitido:

permissions/default.toml
"$schema" = "schemas/schema.json"
[default]
description = "Allows making HTTP requests"
permissions = ["allow-request"]

La forma más fácil de definir permisos para cada uno de tus comandos es utilizar la opción de autogeneración definida en el script de compilación de tu plugin, ubicado en el archivo build.rs. Dentro de la constante COMMANDS, define la lista de comandos en snake_case (debe coincidir con el nombre de la función del comando) y Tauri generará automáticamente los permisos allow-$commandname y deny-$commandname.

El siguiente ejemplo genera los permisos allow-upload y deny-upload:

src/commands.rs
const COMMANDS: &[&str] = &["upload"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

Consulta la documentación de Visión General de Permisos para más información.

Un plugin puede gestionar el estado de la misma manera que lo hace una aplicación Tauri. Lee la Guía de gestión de estado para obtener más información.


© 2026 Colaboradores de Tauri. CC-BY / MIT