@grove-notes/plugin-sdk
v0.1.0
Published
SDK for building Grove plugins: the Plugin base class, plugin context types, registry surface types, and the generated event catalog. The only Grove package a plugin needs to depend on.
Readme
@grove-notes/plugin-sdk
SDK for building Grove plugins: the Plugin base class, plugin context types, registry/surface shapes, and the generated event catalog. The only Grove package a plugin needs to depend on.
This is the runtime API plugin JavaScript imports — distinct from @grove-notes/manifest-schema, which describes the static plugin.json shipping shape an author commits to their repo. Plugins need both: the manifest schema for what they ship, the SDK for what their code links against.
| Package | Audience | What it describes |
|---|---|---|
| @grove-notes/plugin-sdk | Plugin JavaScript authors (only — not themes/icons/templates). | The runtime API a plugin imports: Plugin class, PluginContext, registry shapes (SlashMenuItem, SidebarPanel, …), EventMap. |
| @grove-notes/manifest-schema | Authors of plugins, themes, icon packs, templates; the registry CI. | The static *.json metadata file an extension ships. |
The in-memory LoadedPluginManifest type a plugin sees through this.manifest lives here. The on-disk PluginManifest shape an author writes lives in @grove-notes/manifest-schema. They are different concepts at different abstraction layers.
Install
npm install --save-dev @grove-notes/plugin-sdkQuick start
import { Plugin } from '@grove-notes/plugin-sdk';
export default class MyPlugin extends Plugin {
async onload(): Promise<void> {
this.addCommand({
id: 'open',
title: 'Open my plugin',
command: () => this.logger.info('opened'),
});
this.registerEvent('document.saved', (p) => {
this.logger.info('saved', p.path);
});
}
async onunload(): Promise<void> {
// Plugin-specific teardown only. Every addX() and registerEvent()
// call above is torn down automatically in reverse registration order.
}
}Two ergonomic properties of the base class:
- Auto-cleanup. Every
addCommand/addSidebarPanel/addStatusBarItem/addSettingTab/addDocumentAction/addPropertyEditor/addInspectorView/addFileHandler/registerEvent/registercall records a disposable. On unload, all disposables run in reverse registration order. Plugin authors never callunregistermanually. - ID auto-prefix. Authors write short ids (
{ id: 'open' }); the class prefixes with the plugin name before forwarding to the registry, so two plugins both registeringid: 'open'cannot collide.
The host's contract with your default export
The host expects each plugin module to default-export a constructor that, when called with new Ctor(ctx), produces an instance with _activate() and _deactivate() methods. Extending Plugin inherits both for free — you don't see them; you override onload() / onunload(). Authors who want to roll their own class without the SDK can implement those two methods directly; the host duck-types the shape and does not care about class identity, so bundling a copy of the SDK's Plugin works the same as any other class.
Vanilla JS (no build step)
The host also exposes the runtime Plugin class on globalThis.grove.Plugin, so vanilla-JS plugins without a bundler can write:
const { Plugin } = globalThis.grove;
export default class MyPlugin extends Plugin {
async onload() { /* ... */ }
}This is purely a convenience for the no-build path; bundled plugins should import { Plugin } from '@grove-notes/plugin-sdk' instead.
Exports
Plugin— abstract base class. Extend it; overrideonload()/onunload().PluginContext,PluginLogger,PluginDataApi,PluginPackageJson,PluginWorkspace,LoadedPluginManifest— what the host hands to a plugin and how a plugin's manifest looks once loaded.- Registry surfaces —
SlashMenuItem,SidebarPanel,StatusBarItem,SettingsTab,DocumentAction(+DocumentActionContext),PropertyEditor(+PropertyEditorContext),InspectorView,FileHandler(+FileHandlerContext),SlashCommandContext,RenderFn,Disposable,SurfaceRegistry<T>,RegistriesFacade. EventBus<M>,EventHandler<P>— the typed bus interface plugins receive inctx.bus.EventMap— generated fromcontract/asyncapi.yaml. The single catalog of every typed Grove event and its payload shape.
Compatibility
EventMap and PluginContext are the SDK's contract with plugin code. EventMap is generated from contract/asyncapi.yaml, so adding a new event is a minor bump; renaming or removing one is a major. See docs/adr/event-bus-and-plugin-runtime.md for the lifecycle and docs/features/plugin-runtime.md for the wire surface.
Plugin authors can extend the catalog locally via TS module augmentation:
declare module '@grove-notes/plugin-sdk' {
interface EventMap {
'my-plugin.thing-happened': { id: string };
}
}Versioning & release cadence
The package follows semver:
- Patch — internal changes, no observable surface change.
- Minor — adding optional fields/methods, adding events to
EventMap, broadening accepted shapes. Backward-compatible. - Major — renaming/removing exports, tightening types, removing events.
Release cadence is decoupled from Grove app releases. This package's version, changelog, and publish trigger are independent of the desktop/web app's release cycle. A Grove app release does not auto-publish this package. The current trigger is manual (pnpm --filter @grove-notes/plugin-sdk publish after a version bump, gated by the prepack chain clean → build → test → smoke-test); a tag-based GitHub Action keyed on plugin-sdk-v* is follow-up work.
License
MIT. Plugin authors of any license — including proprietary or GPL-incompatible projects — can depend on this package without copyleft concerns. The host process and the SDK contract are deliberately decoupled licensing-wise so the plugin ecosystem can grow without forcing a single license on every author.
