npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

react is a peer dependency — the SDK does not bundle React. Plugins that don't use React can omit it (the ./hooks subpath 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 assets

Programmatic 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>.rbp

Exit 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