feat: v0.1.0 — Studio Plugin SDK

First release of the SDK that Studio plugins depend on.

What v0.1 ships:

- WIT contract under wit/ (studio-plugin.wit +
  deps/platform/world.wit, both verbatim from
  fai/platform/wit/).
- wit-bindgen 0.36 generate! call with pub_export_macro
  enabled so the emitted __export_studio_plugin_impl! macro
  is reachable from downstream plugin crates.
- export_plugin!(MyType) convenience macro that wraps the
  bindgen-generated impl macro.
- README documenting usage + v0.1 limits (single-world ->
  all-three-hooks-required, no proc-macro yet, edition
  2021 pinned by wit-bindgen output).

What v0.1 doesn't yet do:

- Plugins still declare all three Guest impls (theme,
  translate, output-view). v0.2 splits the WIT into one
  world per hook.
- No #[plugin(...)] proc-macro. v0.2 wraps generate! +
  Guest impl generation into a single attribute.
- Host-side: the hub doesn't yet load studio-plugin-world
  components or expose InvokePlugin RPC — that's the next
  push.

Validated against the in-tree studio-theme-solarized
plugin: 22 KiB stripped .wasm artefact, valid Component
Model, passes wasm-tools component wit inspection.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-05-25 14:31:08 +02:00
commit 3753c6bc37
7 changed files with 562 additions and 0 deletions

104
src/lib.rs Normal file
View file

@ -0,0 +1,104 @@
//! `fai-studio-plugin-sdk` — ergonomic wrapper around the
//! Studio-plugin WIT contract.
//!
//! Studio plugins are WASM components targeting the
//! `studio-plugin` world declared in `wit/studio-plugin.wit`
//! (a verbatim copy of `fai/platform`'s authoritative file).
//! They export between one and three hooks — `translate`,
//! `output-view`, `theme` — and import the same `host`
//! interface every flow module uses for structured logging.
//!
//! # SKELETON
//!
//! v0.1 of this crate gives plugins:
//!
//! * `wit_bindgen!`-generated bindings re-exported under
//! [`bindings`] so plugins reference one stable name.
//! * Ergonomic re-exports for the most-used types
//! ([`ColorScheme`], [`PluginError`], [`RenderedOutput`]).
//! * An `export_plugin!` macro that hides the `export!`
//! incantation. v0.1 takes a single struct that
//! implements one or more `Guest` traits; v0.2 will
//! turn this into a proper proc-macro
//! (`#[plugin(theme, translate)]`).
//!
//! What v0.1 deliberately does NOT yet do:
//!
//! * Generate any glue for the *host* side (the hub still
//! needs a dedicated component loader for
//! `studio-plugin`-world components — that lands as part
//! of the `InvokePlugin` RPC implementation).
//! * Provide testing helpers. Plugins author their own
//! unit tests against the trait impls directly.
#![allow(missing_docs)]
/// Raw bindings generated from the WIT contract. Plugins
/// access types through the convenient re-exports below in
/// most cases; this module is here for when they need the
/// underlying definitions (e.g. to implement multiple Guest
/// traits in one struct).
pub mod bindings {
wit_bindgen::generate!({
world: "studio-plugin",
path: "wit",
// Generate bindings for the entire WIT package
// (incl. fai:platform/host that studio-plugin imports
// for log emission), so plugins don't need a separate
// `with: …` mapping.
generate_all,
// Export the `export!` macro publicly so downstream
// plugin crates can invoke it via the SDK's
// `export_plugin!` wrapper without each plugin
// re-running `wit_bindgen::generate!()` against a
// copy of the WIT files.
pub_export_macro: true,
});
}
// v0.1: plugin authors reference paths inside `bindings::`
// directly. The typical ones are:
//
// bindings::exports::fai::studio_plugin::theme::Guest
// bindings::exports::fai::studio_plugin::translate::Guest
// bindings::exports::fai::studio_plugin::output_view::Guest
// bindings::fai::studio_plugin::plugin_types::ColorScheme
// bindings::fai::studio_plugin::plugin_types::PluginError
// bindings::fai::studio_plugin::plugin_types::RenderedOutput
//
// Stable re-exports under terser names land in v0.2 — they
// have to wait for the proc-macro that materialises the
// `Guest` impls anyway.
/// Convenience constructor: declare a plugin component by
/// pointing this macro at the struct that implements the hook
/// `Guest` traits.
///
/// ```ignore
/// use fai_studio_plugin_sdk::{export_plugin, bindings};
///
/// struct MyTheme;
/// impl bindings::exports::fai::studio_plugin::theme::Guest for MyTheme {
/// fn theme_for(b: String)
/// -> Result<bindings::fai::studio_plugin::plugin_types::ColorScheme,
/// bindings::fai::studio_plugin::plugin_types::PluginError>
/// {
/// // ...
/// }
/// }
///
/// export_plugin!(MyTheme);
/// ```
///
/// One struct can implement multiple `Guest` traits — the SDK
/// does not require a separate struct per hook.
///
/// Under the hood this resolves to the `__export_studio_plugin_impl!`
/// macro that `wit_bindgen::generate!(pub_export_macro: true)`
/// emitted at the crate root.
#[macro_export]
macro_rules! export_plugin {
($struct:ident) => {
$crate::__export_studio_plugin_impl!($struct with_types_in $crate::bindings);
};
}