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.

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

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

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:

  • current holds 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 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 rewrites importFrom to 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.