Client Scripts & Client Context

A dock client script runs a plugin's code inside the host page; the client context is how client surfaces reach the hub.

A dock client script runs a plugin's code inside the host page; the client context is how client surfaces reach the hub.

Experimental The hub API surface is still being refined. Names may change before 1.0.

The client host runtime

createDevframeClientHost() (@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 app / hub page's browser entry
import { connectDevframe, createDevframeClientHost } from '@devframes/hub/client'

const rpc = await connectDevframe({ baseURL: '/__hub/' })
const { context, dispose } = await createDevframeClientHost({ rpc })

Options

OptionDescription
rpcAn already-connected DevframeRpcClient; when omitted, created via connectDevframe(connect).
connectForwarded to connectDevframe when rpc is omitted (e.g. baseURL).
clientType'standalone' (default) — owns the page; 'embedded' — inside a user app alongside a panel.
loadClientScriptsImport and run dock client scripts (default true).
renderersDock renderers registered at boot, keyed by dock type; local wins over the hub's renderer manifest.

A second boot replaces the context and warns; dispose() tears down listeners and unpublishes it.

The client context

PropertyDescription
rpcThe RPC client — server/client functions, shared state.
clientType'embedded' (inside your app) or 'standalone' (independent hub page).
docksentries, selected, groupedEntries, switchEntry(), toggleEntry(), getStateById(), register() / update() for client-only docks.
panelDock panel state: position, size, drag/resize.
commandsCommand palette: register(), execute(), getKeybindings().
renderersDock-renderer registry — register(), get(), has(), mount(entry, container). Routes a dock type to a renderer (local boot or the hub's manifest; local wins). mount() resolves a status: mounted (with dispose), missing-renderer, or load-error (with error).
whenThe when-clause context.
connectionLive connection statusstatus, error, events.

Accessing the context

getDevframeClientContext() returns the context anywhere; undefined before boot.

Client-only docks

A client host can register a dock local to this 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:

Entry kindFieldRuns
actionactionwhen the dock button is activated
custom-renderrendererto render the entry's panel
iframeclientScript (optional)alongside the iframe panel, inside the host page

The exported function (DockClientScriptContext) receives the client context and two dock-scoped extras:

  • current — 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 (category defaults to the entry id; info/warn/error/success/debug shortcuts for add()).

A failed import retries on the next dock update.

Shipping a client script

importFrom accepts two shapes:

  • A host-served URL — a self-contained ES module; works on every host.
  • A bare npm specifier ('vite-plugin-vue-tracer/client/vite-devtools') — resolved through the host runtime.

For a URL, attach it via ctx.install(myDevframe, { dock: { clientScript: { importFrom } } }). Under Vite /@fs/<absolute path> serves it; other hosts mount the directory statically.

Bare npm specifiers

Bare specifiers are a host-runtime capability: a host 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 with no template (Next.js) supports the URL shape only, warning DF8111 on a specifier. A viewer can override with createDevframeClientHost({ resolveClientModule }).

Client scripts execute in the inspected page's 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 in-page agent (a11y inspector).

Iframe panels

Dock iframes are their own documents: the panel SPA calls connectDevframe(), discovering ./__connection.json from its base. Host script and iframe share the server via RPC and shared state, or a same-origin BroadcastChannel for static builds.

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 host attaches a frame-nav adapter speaking an origin-locked postMessage protocol on devframe:frame-nav.

MessageDirectionMeaning
ready / manifestframe → hosttab list ({ tabs, current }), on load and change
navigatehost → frameshow a view ({ tabId, navTarget }); app routes client-side
navigatedframe → hostapp navigated internally; host highlights the dock

It materializes a client-only dock per tab (id <frameId>:<tabId>) sharing the anchor's frameId and a navTarget, independent of groupId.

The viewer's part

A viewer 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/hub-vite / hub-next.