@rbrowser/plugin-sdk
v0.1.0
Published
Public SDK for building RNA Browser plugins.
Readme
@rbrowser/plugin-sdk
Public SDK for building plugins for RBrowser.
Install
npm install --save-peer react
npm install @rbrowser/plugin-sdk
reactis a peer dependency — the SDK does not bundle React. Plugins that don't use React can omit it (the./hookssubpath is the only React-dependent entry point).
Quick start
A minimal RBrowser plugin in three files:
manifest.json
{
"id": "com.example.hello",
"name": "Hello RBrowser",
"version": "0.1.0",
"apiVersion": "1",
"minHostVersion": "0.1.0",
"entry": "index.js",
"permissions": ["state:read", "navigation:write", "highlight:write"],
"panel": { "position": "right", "title": "Hello", "defaultWidth": 280 }
}src/index.tsx
import { createRoot } from 'react-dom/client'
import { definePlugin } from '@rbrowser/plugin-sdk'
import { RBrowserHostProvider, useSelectedRNA, useSpecies } from '@rbrowser/plugin-sdk/hooks'
function Panel() {
const species = useSpecies()
const rna = useSelectedRNA()
return (
<div style={{ padding: 12 }}>
<h3>Hello, {species.name}!</h3>
{rna ? <p>Selected: {rna.transcriptName ?? rna.transcriptId}</p> : <p>No transcript selected.</p>}
</div>
)
}
export default definePlugin({
render(container, host) {
createRoot(container).render(
<RBrowserHostProvider host={host}>
<Panel />
</RBrowserHostProvider>
)
}
})Pack and install
# build your bundle (e.g. via Vite/Rollup) → dist/index.js
npx rbrowser-plugin pack --manifest manifest.json --bundle dist/index.js --out dist/hello.rbp
# Open RBrowser → Plugin Store → drag dist/hello.rbp into the panel.API reference
Authoring
| Symbol | Purpose |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| definePlugin(m) | Identity helper that types your module as a PluginModule. Use as export default definePlugin({...}). |
| PluginModule | { activate?, render, deactivate? } — lifecycle the host calls on you. |
| PluginManifest | Shape of manifest.json. See "Manifest" below for fields. |
| Permission | Union: 'state:read' \| 'navigation:write' \| 'highlight:write' \| 'storage:write'. |
PluginModule lifecycle:
interface PluginModule {
activate?(host: RBrowserHost): void | Promise<void> // once, after manifest verified
render(container: HTMLElement, host: RBrowserHost): void | Promise<void> // every mount
deactivate?(): void | Promise<void> // on unload; release resources
}Host bridge
The host passes you a RBrowserHost in activate / render:
interface RBrowserHost {
readonly info: HostInfo // pluginId, permissions, host & API versions
getState(): RBrowserState // snapshot of all keys
subscribe<K>(key: K, cb: (v: RBrowserState[K]) => void): Unsubscribe
subscribeAll(cb: (s: RBrowserState) => void): Unsubscribe
navigate(target: NavigateTarget): void // region or transcript
highlight: HighlightApi // add / remove / clear named regions
storage: PluginStorage // per-plugin namespaced KV
}RBrowserState — every read-only key your plugin can observe:
| Key | Type | Description |
| ------------------ | ----------------------------------------- | ------------------------------------------ |
| species | { name, assembly } | Current organism + reference (e.g. GRCh38) |
| selectedRNA | SelectedRNA \| null | Transcript currently selected by the user |
| channels | ChannelSnapshot[] | All visible data channels (plot/level/url) |
| favorites | FavoriteEntry[] | User's saved regions |
| history | HistoryEntry[] | Recently-visited regions (region + ts) |
| highlightRegions | { DNA, RNA, mRNA, CDS } highlight lists | Named highlight regions per coordinate |
navigate target shapes:
host.navigate({ chr: 'chr1', start: 7766295, end: 7786432 })
host.navigate({ transcriptId: 'ENST00000409539', padding: 1000 })highlight — name-keyed regions overlaid on the renderer:
host.highlight.add({ name: 'binding-site', start: 100, end: 150, color: '#ff8800' })
host.highlight.remove(0) // by index in the mRNA list
host.highlight.clear()storage — per-plugin namespaced key/value (localStorage-backed):
host.storage.set('lastQuery', 'BRCA1')
host.storage.get('lastQuery') // 'BRCA1' | null
host.storage.keys() // string[]
host.storage.remove('lastQuery')
host.storage.clear()React hooks (@rbrowser/plugin-sdk/hooks)
Wrap your tree in a provider and call hooks anywhere inside:
import { RBrowserHostProvider, useRBrowserHost } from '@rbrowser/plugin-sdk/hooks'
;<RBrowserHostProvider host={host}>
<YourUI />
</RBrowserHostProvider>Reading styles, all backed by useSyncExternalStore with per-key subscriptions:
// 1. Per-key (recommended)
import {
useSpecies,
useSelectedRNA,
useChannels,
useFavorites,
useHistory,
useHighlightRegions
} from '@rbrowser/plugin-sdk/hooks'
const species = useSpecies()
// 2. Namespaced (state-path-style call sites)
import { RBrowserState } from '@rbrowser/plugin-sdk/hooks'
const rna = RBrowserState.selectedRNA()Storage hook:
import { useRBrowserStorage } from '@rbrowser/plugin-sdk/hooks'
const [query, setQuery] = useRBrowserStorage('lastQuery', '')Access the bridge itself for imperative calls (navigate, highlight):
const host = useRBrowserHost()
host.navigate({ chr: 'chr1', start: 100, end: 200 })Manifest
Fields recognized by the loader and surfaced in the Plugin Store UI:
| Field | Required | Notes |
| ---------------- | -------- | -------------------------------------------------------------------- |
| id | yes | Stable unique id (reverse-DNS or kebab-case). |
| name | yes | Human-readable name. |
| version | yes | Plain semver. Used for upgrade-vs-downgrade detection. |
| apiVersion | yes | '1' — bumped on breaking SDK changes. |
| minHostVersion | yes | Lowest host semver this plugin works on. |
| entry | yes | Relative path to the ESM bundle (default-exports PluginModule). |
| permissions | yes | Array of Permission strings; shown to the user on install. |
| panel | yes | { position: 'left' \| 'middle' \| 'right', title?, defaultWidth? } |
| author | no | |
| description | no | One-line description shown in the store. |
| icon | no | Path inside the .rbp archive (typically icon.png). |
| homepage | no | URL to docs / source repo. |
.rbp packaging
.rbp is a standard ZIP. Layout:
my-plugin.rbp
├── manifest.json ← required
├── index.js ← required, path = manifest.entry
├── icon.png ← optional, displayed in the Plugin Store
├── data/... ← optional dataset files
└── assets/... ← optional static assetsProgrammatic API:
| Function | Description |
| ------------------------------- | ---------------------------------------------------------------------------- |
| packRbp(input) | Build .rbp bytes from { manifest, bundleSource, iconBytes?, extra? }. |
| unpackRbp(bytes) | Parse .rbp → { manifest, bundleSource, bundleBytes, iconBytes?, files }. |
| looksLikeRbp(bytes) | Magic-byte sniff (PK\x03\x04). |
| looksLikeLegacyJsonRbp(bytes) | Detect old v1 (JSON) .rbp so callers can suggest a repack. |
| validateManifestForPacking(m) | Structural check; returns error message or null. |
| rbpFilename(manifest) | Canonical <id>-<version>.rbp filename. |
Constants: RBP_FORMAT_VERSION ('rbp-v2'), RBP_MANIFEST_PATH ('manifest.json'), RBP_ICON_PATH ('icon.png').
CLI
rbrowser-plugin pack [options]
--manifest <path> default ./manifest.json
--bundle <path> default ./dist/index.js
--icon <path> optional
--extra <dir> optional, may be repeated (e.g. --extra ./data --extra ./assets)
--out <path> default ./dist/<id>-<version>.rbpExit codes: 0 success, 1 usage / I/O error, 2 manifest validation error.
Debug bridge
In dev mode only, the host publishes window.__rbrowser__.connect(pluginId). getDebugHost(pluginId?) is a typed wrapper for DevTools experiments:
import { getDebugHost } from '@rbrowser/plugin-sdk'
const host = getDebugHost('com.example.hello')
host.getState()Production plugins must not call this — they receive a capability-scoped host from the loader.
Full example
A plugin that watches the selected transcript and lets the user click a favourite to navigate there. Uses hooks, navigation, and storage.
import { createRoot } from 'react-dom/client'
import { definePlugin } from '@rbrowser/plugin-sdk'
import {
RBrowserHostProvider,
useRBrowserHost,
useSelectedRNA,
useFavorites,
useRBrowserStorage
} from '@rbrowser/plugin-sdk/hooks'
function Panel() {
const host = useRBrowserHost()
const rna = useSelectedRNA()
const favorites = useFavorites()
const [note, setNote] = useRBrowserStorage('lastNote', '')
return (
<div style={{ padding: 12, fontSize: 13 }}>
<h4>Current transcript</h4>
{rna ? (
<p>
<strong>{rna.transcriptName ?? rna.transcriptId}</strong>
{rna.geneName ? ` (${rna.geneName})` : ''}
</p>
) : (
<p style={{ color: '#888' }}>Nothing selected.</p>
)}
<h4>Favorites</h4>
<ul>
{favorites.map((f, i) => (
<li key={i}>
<button
onClick={() => {
const m = /^([\w.]+):(\d+)-(\d+)$/.exec(f.region)
if (m) host.navigate({ chr: m[1], start: +m[2], end: +m[3] })
}}>
{f.region}
</button>
</li>
))}
</ul>
<h4>Note (persists per-plugin)</h4>
<textarea value={note} onChange={(e) => setNote(e.target.value)} rows={3} style={{ width: '100%' }} />
</div>
)
}
export default definePlugin({
activate(host) {
console.log(`[hello] activated as ${host.info.pluginId}`)
},
render(container, host) {
createRoot(container).render(
<RBrowserHostProvider host={host}>
<Panel />
</RBrowserHostProvider>
)
},
deactivate() {
console.log('[hello] deactivated')
}
})Build the bundle, run rbrowser-plugin pack, drag the resulting .rbp into the RBrowser Plugin Store, and the panel appears on the right side.
License
MIT
