Client Scripts & Client Context
A dock client script runs a devframe's code inside the host page; the client context is how client scripts reach the hub.
The client runtime
createDevframeClientRuntime() (@devframes/hub/client) boots the host page: it connects (or adopts) an RPC client, publishes the DevframeClientContext, and imports each dock's client script:
// main.ts: the host page's browser entry
import { connectDevframe, createDevframeClientRuntime } from '@devframes/hub/client'
const rpc = await connectDevframe({ baseURL: '/__hub/' })
const { context, dispose } = await createDevframeClientRuntime({ rpc })Options
Pass an already-connected rpc (or connect options for connectDevframe), the page's clientType ('standalone' by default, 'embedded' inside a user app), loadClientScripts: false to skip dock client scripts, and boot-time renderers (which win over the hub's renderer manifest); see the Hub API reference.
A second boot replaces the context and warns; dispose() tears down listeners and unpublishes it.
The client context
The context carries the RPC client (rpc) and the page's clientType, plus six hub facets: docks (entries, selection, client-only registration), panel (dock panel state), commands (the command palette), renderers (the dock-renderer registry), when (the when-clause context), and connection (live connection status). Every property is in the Hub API reference.
Accessing the context
getDevframeClientContext() returns the context anywhere; undefined before boot.
Tracking panel state
ctx.panel.state is the current dock panel snapshot. It contains state: 'open' | 'closed' | 'hidden' and includes selectedDockId while a dock is selected. Subscribe to ctx.panel.events for later changes:
import type { DockClientScriptContext } from '@devframes/hub/client'
import { HUB_EVENTS } from '@devframes/hub/constants'
export default function setup(context: DockClientScriptContext) {
const reportPanelState = (panelState: typeof context.panel.state) => {
void context.rpc.call('my-devframe:panel-state', panelState).catch(error => console.error(error))
}
reportPanelState(context.panel.state)
context.panel.events.on(
HUB_EVENTS.client.docksPanelStateChanged,
reportPanelState,
)
}The custom RPC keeps node-side reporting opt-in.
Client-only docks
A client runtime can register a dock local to the host page (unlike node hub context docks synced via devframe:docks). ctx.docks.register(entry) (e.g. type: 'custom-render' with renderer: { importFrom }) returns a handle whose update({ badge }) patches in place (id immutable) and dispose() removes it. One sharing a server dock's id overrides it locally; re-registering an owned id throws unless you pass register(entry, true).
A client-only dock can also carry type: 'json-render' with an inline JSON-render view: { spec } (a DevframeJsonRenderSpec built in-browser), rendered when a json-render renderer is registered at boot. view also accepts { stateKey } for live shared state (from createJsonRenderView).
Dock client scripts
A client script is a ClientScriptEntry: { importFrom, importName? } (importName defaults 'default'). The field varies by entry kind: an action entry's action runs when the dock button is activated, a custom-render entry's renderer renders its panel, and an iframe entry's optional clientScript runs alongside the iframe panel inside the host page (Hub API reference).
The exported function (DockClientScriptContext) receives the client context and two dock-scoped extras:
currentholds this entry's state:entryMeta,isActive,domElements,events(entry:activated,entry:deactivated,entry:updated,dom:panel:mounted,dom:iframe:mounted).messages: an entry-scoped messages client (categorydefaults to the entry id;info/warn/error/success/debugshortcuts foradd()).
A failed import retries on the next dock update.
Shipping a client script
importFrom accepts three shapes:
- A URL served by the host framework: a self-contained ES module; works on every host framework.
- A bare npm specifier (
'vite-plugin-vue-tracer/client/vite-devtools'): resolved through the host framework. - An absolute filesystem path, declared on the definition's
dock.clientScript. The hub serves its directory under<base>__page-script/and rewritesimportFromto that URL, so mounting by package name needs no host wiring.
Per-mount, attach a URL via ctx.install(myDevframe, { dock: { clientScript: { importFrom } } }); under Vite /@fs/<absolute path> serves it, and other host frameworks mount the directory statically.
Bare npm specifiers
Resolving a bare specifier is the host framework's capability: a host framework advertises a resolution template at ConnectionMeta.configs.dock.clientModuleResolution (loaders replace {specifier} before import). A Vite host declares this by default (@devframes/vite/hub) as initHub({ clientModuleResolution: '/@id/{specifier}' }); then use the specifier alone as importFrom. A host framework with no template (Next.js) supports the URL shape only, warning DF8111 on a specifier. A hub UI provider can override with createDevframeClientRuntime({ resolveClientModule }).
Client scripts execute in the user app's page realm (window); anchor shared state on globalThis.
Dual boots
One bundle can serve as both a client script (default export) and, via a globally-guarded self-boot, a standalone page script (a11y inspector).
Iframe panels
Dock iframes are their own documents: the panel calls connectDevframe(), discovering ./__connection.json from its base. A client script and an iframe panel share the node side via RPC and shared state, or talk directly (server-free, static-build-friendly) over the in-page channel.
Shared-iframe soft navigation
A tool with many internal views (Nuxt DevTools' tabs) can surface each as a hub dock sharing one live iframe: the anchor owns a frameId and opts in via ctx.install(…, { dock: { frameId, subTabs: { protocol: 'postmessage' } } }). On mount, the client runtime attaches a frame-nav adapter speaking an origin-locked postMessage protocol on devframe:frame-nav: the iframe reports its tab list (ready / manifest), the host page requests a view (navigate), and the iframe reports internal navigation back (navigated). Message shapes are in the Hub API reference.
It materializes a client-only dock per tab (id <frameId>:<tabId>) sharing the anchor's frameId and a navTarget, independent of groupId.
The hub UI provider's part
A hub UI provider keeps one iframe alive per frameId (shown/hidden); on mount, it sets the element on the anchor's docks.getStateById(anchorId) state (domElements.iframe) and emits dom:iframe:mounted. See the "Tabbed Tool" in examples/custom-hub-vite / custom-hub-next.
Hub
@devframes/hub orchestrates many devtools sharing a UI: a dock registry, terminal aggregation, message/toast queue, and command palette. It ships no UI; hub UI providers provide their own atop the hub's RPC + shared-state protocol.
Serve a Hub Anywhere
initHub() from @devframes/hub/initiate serves a whole multi-devframe devtools install from one web-standard handler on a catch-all route.