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

@rn-devtools/plugin-sdk

v1.0.3

Published

Plugin SDK for rn-devtools

Downloads

18

Readme

@rn-devtools/plugin-sdk

Tools and utilities for building rn-devtools plugins on both the dashboard (web) and the device (native) sides.


Installation

# with yarn
yarn add -D @rn-devtools/plugin-sdk

# or with npm
npm i -D @rn-devtools/plugin-sdk

Peer dependency: socket.io-client v4 is expected in apps that use this SDK.


Quick Start

1) Pick a pluginId

Use a stable string, e.g. "rn-devtools.demo.counter".

2) Native (device) side

Create a client bound to your pluginId, the active socket, and the current deviceId.

import type { Socket } from 'socket.io-client';
import { createNativePluginClient } from '@rn-devtools/plugin-sdk';

// somewhere in your native runtime
function makeNativeBus(socket: Socket, deviceId: string) {
  const pluginId = 'rn-devtools.demo.counter';
  const bus = createNativePluginClient(pluginId, socket, deviceId);

  // send an event up to the dashboard
  bus.sendMessage('counter:value', { value: 42 });

  // listen for commands coming down from the dashboard
  const unsubscribe = bus.addMessageListener<{ step: number }>('counter:inc', ({ step }) => {
    // increment your native counter by `step`
  });

  // later, on cleanup
  return () => unsubscribe();
}

3) Dashboard (web) side

Create a client for the same pluginId. Provide a function that returns the currently selected device id.

import * as React from 'react';
import { createWebPluginClient } from '@rn-devtools/plugin-sdk';

export function CounterPanel({ currentDeviceId }: { currentDeviceId?: string }) {
  const pluginId = 'rn-devtools.demo.counter';
  const bus = React.useMemo(
    () => createWebPluginClient(pluginId, () => currentDeviceId),
    [pluginId, currentDeviceId]
  );

  React.useEffect(() => {
    // listen for values coming from the device
    const unsub = bus.addMessageListener<{ value: number }>('counter:value', (payload, { deviceId }) => {
      console.log('value from', deviceId, payload.value);
    });
    return () => unsub();
  }, [bus]);

  return (
    <button onClick={() => bus.sendMessage('counter:inc', { step: 1 })}>
      Increment on device
    </button>
  );
}

Message Flow

Dashboard (web)                           Device (native)
------------------------------------     ------------------------------------
sendMessage(event, payload, deviceId)  →  socket.emit("plugin:down", { pluginId, deviceId, event, payload })
                                          ↑
socket.on("plugin:up", handler)       ←   sendMessage(event, payload)
                                          socket.emit("plugin:up", { pluginId, deviceId, event, payload, timestamp })
  • On dashboard init, a singleton socket is created and says hello:

    • socket = io('/', { transports: ['websocket', 'polling'] })
    • socket.emit('devtools:hello', { role: 'dashboard' })

API Reference

createNativePluginClient(pluginId, socket, deviceId) => NativeBus

Builds a message bus bound to a specific device and plugin.

export type NativeBus = {
  sendMessage: (event: string, payload?: unknown) => void;
  addMessageListener: <T = unknown>(event: string, cb: (payload: T) => void) => () => void;
};
  • sendMessage(event, payload?) → emits plugin:up with { pluginId, deviceId, event, payload, timestamp }.
  • addMessageListener(event, cb) → listens to plugin:down and filters by { pluginId, deviceId, event }. Returns an unsubscribe function.

createWebPluginClient(pluginId, getDeviceId) => WebBus

Creates or reuses a singleton socket and builds a bus scoped to your plugin.

export type WebBus = {
  socket: Socket; // shared across plugins
  sendMessage: (event: string, payload?: unknown, deviceId?: string) => void;
  addMessageListener: <T = unknown>(
    event: string,
    cb: (payload: T, meta: { deviceId: string }) => void,
  ) => () => void;
};
  • sendMessage(event, payload?, deviceId?) → emits plugin:down to the resolved device id (deviceId ?? getDeviceId()); no-op if no device id is available.
  • addMessageListener(event, cb) → listens to plugin:up for your pluginId and event. The callback receives { payload, meta: { deviceId } }. Returns an unsubscribe function.

Note: The dashboard socket is a singleton. All plugins share the same io('/') connection.


Shared Types

export type Device = {
  id: string;
  deviceId: string;
  deviceName: string;
  isConnected: boolean;
  platform?: string;
};

export type PluginProps = {
  targetDevice: Device;      // the currently focused device
  allDevices: Device[];      // every known device
  isDashboardConnected: boolean;
};

export type PluginMsg = {
  pluginId: string;
  deviceId?: string;
  event: string;
  payload?: Record<string, unknown>;
  timestamp?: number; // set by native on plugin:up
};

export type DevtoolsPlugin = {
  id: string; // your pluginId
  title: string; // display name
  Icon: React.FC<{ className?: string }>;
  mount: React.ComponentType<PluginProps>; // the main React view of your plugin
};

export type NativeHookProps = {
  socket: Socket;
  deviceId: string;
};

Example: minimal DevtoolsPlugin

import type { DevtoolsPlugin, PluginProps } from '@rn-devtools/plugin-sdk';
import { createWebPluginClient } from '@rn-devtools/plugin-sdk';

function Panel(props: PluginProps) {
  const bus = React.useMemo(
    () => createWebPluginClient('rn-devtools.demo.counter', () => props.targetDevice?.deviceId),
    [props.targetDevice?.deviceId]
  );

  // ...render UI and talk to native via `bus`
  return null;
}

export const CounterPlugin: DevtoolsPlugin = {
  id: 'rn-devtools.demo.counter',
  title: 'Counter',
  Icon: (p) => <svg {...p} viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>,
  mount: Panel,
};

Events & Conventions

  • Upstream (native → web): plugin:up with { pluginId, deviceId, event, payload, timestamp }
  • Downstream (web → native): plugin:down with { pluginId, deviceId, event, payload }
  • Handshake (web only): devtools:hello with { role: 'dashboard' } emitted once per session
  • Unsubscribe early: always call the function returned by addMessageListener in useEffect cleanup or component teardown

Gotchas & Notes

  • The dashboard client ignores sends if it cannot resolve a device id. Make sure getDeviceId() is stable and returns a value when your UI is active.

  • The native client filters by exact pluginId, deviceId, and event. Make sure they match on both ends.

  • timestamp is added on native plugin:up messages; the dashboard does not set it.

  • If you want to support broadcasts from native (i.e., deviceId omitted), confirm your filtering logic. A common pattern is:

    // Accept messages for the current device *or* broadcasts
    if (msg.deviceId !== undefined && msg.deviceId !== current) return;

TypeScript Tips

  • Use generics on addMessageListener<T>() to get typed payloads per event.
  • Consider building a union of event→payload mappings for your plugin and wrapping sendMessage/addMessageListener to constrain event names.