@rn-devtools/plugin-sdk
v1.0.3
Published
Plugin SDK for rn-devtools
Downloads
18
Maintainers
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-sdkPeer dependency:
socket.io-clientv4 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?)→ emitsplugin:upwith{ pluginId, deviceId, event, payload, timestamp }.addMessageListener(event, cb)→ listens toplugin:downand 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?)→ emitsplugin:downto the resolved device id (deviceId ?? getDeviceId()); no-op if no device id is available.addMessageListener(event, cb)→ listens toplugin:upfor yourpluginIdand 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:upwith{ pluginId, deviceId, event, payload, timestamp } - Downstream (web → native):
plugin:downwith{ pluginId, deviceId, event, payload } - Handshake (web only):
devtools:hellowith{ role: 'dashboard' }emitted once per session - Unsubscribe early: always call the function returned by
addMessageListenerinuseEffectcleanup 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, andevent. Make sure they match on both ends.timestampis added on nativeplugin:upmessages; the dashboard does not set it.If you want to support broadcasts from native (i.e.,
deviceIdomitted), 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/addMessageListenerto constrain event names.
