Skip to content

[Proposal]: Plugin Builder #2959

@JonasKruckenberg

Description

@JonasKruckenberg

This proposal is not finished yet! Please participate by reviewing the proposed API.

Is your feature request related to a problem? Please describe.

There are several observations I made about the current Plugin trait that I would like to address:

  1. Defining a custom struct for a plugin has little realworld use as plugin state is better stored using the app.manage and app.state methods. (This state is also accessible in commands while plugin struct fields are not)
  2. the Plugin method names are not super self explanatory and naming is somewhat inconsistent (created is an event callback but is not named on_created while on_event and on_page_load are)
  3. The practice of having an invoke_handler field and a extend_api to register commands is not obvious
  4. In "real-world" usage (the official plugins) the config property taken from the tauri.conf.json file had little use and makes plugins more confusing rather than easier to use. It has shown that exporting a builder and having configuration "in-code" is better.

In general I think the current Plugin api leads to unnecessarily verbose and complex plugins, so I would like to propose an improved builder based API:

Describe the solution you'd like
Introduce a PluginBuilder struct to streamline the plugin creation:

/// Mock command
#[command]
async fn execute<R: Runtime>(_app: AppHandle<R>) -> Result<String> {
  Ok("success".to_string())
}

pub fn init() -> Plugin { // not the Plugin trait
   PluginBuilder::new("example")
      .commands(vec![execute])
      .setup(|app| {
         app.manage(MyState::default());
         Ok(())
      })
}

Instead of the current api:

/// Mock command
#[command]
async fn execute<R: Runtime>(_app: AppHandle<R>) -> Result<String> {
  Ok("success".to_string())
}

pub struct ExamplePlugin<R: Runtime> {
  invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}

impl<R: Runtime> Default for ExamplePlugin<R> {
  fn default() -> Self {
    Self {
      invoke_handler: Box::new(tauri::generate_handler![execute]),
    }
  }
}

impl<R: Runtime> Plugin<R> for ExamplePlugin<R> {
  fn name(&self) -> &'static str {
    "example"
  }

  fn initialize(&mut self, app: &AppHandle<R>, _config: JsonValue) -> tauri::plugin::Result<()> {
    app.manage(MyState::default());
    Ok(())
  }

  fn extend_api(&mut self, message: Invoke<R>) {
    (self.invoke_handler)(message)
  }
}

The proposed builder methods closely resemble the traits methods, with a few differences:

  • rename methods to better communicate their purpose
    js instead of initiailization_script, commands instead of extend_api and setup instead of initialize.
  • name event callbacks consistently
    with the prefix on_
trait PluginBuilder {
   // creates the builder using the plugin `name`
   fn new(name: &'static str) -> Self;
   // Renamed `initialization_script` fn to better communicate it takes a JavaScript string
   fn js(&mut self, js_code: String) -> &mut self;
   // Register an array of tauri commands. This hides all the ugly Boxing and macro usage
   fn commands(&mut self, commands: Vec<?>) -> &mut self;
   // Renamed `initialize` fn to highlight the similarity to the `AppBuilder`s setup fn
   fn setup(setup: Fn(&AppHandle<R>) -> Result<(), Box<dyn Error + Send>> + Send + 'static);
   // set the `on_event ` method
   fn on_event(cb: Fn(&AppHandle<R>, &Event)) -> &mut self;
   // set the `on_page_load` method
   fn on_page_load(cb: Fn(Window<R>, payload: PageLoadPayload)) -> &mut self;
   // set the `created` method
   fn on_webview_ready(cb: Fn(Window<R>)) -> &mut self;
   // builds the plugin
   fn build(self) -> Plugin; // not the Plugin trait
}

The PluginBuilder struct could just be wrapper returning Plugin trait implementations, so this proposal would have a clear implementation path.

Notes

  1. Having a builder struct allows us to improve and extend the plugin API in the future with less hassle
  2. I propose plugins exporting a creation function called init (as shown in the example above). This function is free to receive any arguments, so plugin authors may use this to provide configuration options for end users.
  3. I'm considering to remove the js/initialization_script methods all together. Plugins should expose corresponding js packages and not rely on extending the global object. The overwhelming majority of tauri projects also uses bundlers which make providing api packages is a no-brainer. For the few projects that don't, plugin can provide prebuilt browser bundles. See tauri-plugin-store for an example of this.

Describe alternatives you've considered
Leave the trait as is.

Additional context
The builder pattern is inspired by deno_cores ExtensionBuilder.
The initfunction is inspired by denos extensions and the convention among rollup plugins to export a single creation function.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions