Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/content/1.guide/17.client-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ A failed import retries on the next dock update.

### Shipping a client script

`importFrom` accepts two shapes:
`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.

For a URL, attach it via `ctx.install(myDevframe, { dock: { clientScript: { importFrom } } })`. Under Vite `/@fs/<absolute path>` serves it; other host frameworks mount the directory statically.
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

Expand Down
10 changes: 9 additions & 1 deletion docs/content/5.plugins/4.a11y.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ The page script and the panel talk over the [in-page channel](/guide/in-page-cha

## In a hub

The page script is the a11y dock's [client script](/guide/client-context): attach `a11yPageScriptBundlePath` as the dock's `clientScript` and the hub imports it into the page. It also mirrors each scan into the hub's messages feed — a summary plus one per rule:
The definition declares the page script as its dock [client script](/guide/client-context), so mounting by package name just works:

```ts
initHub({ devframes: ['@devframes/plugin-a11y'] })
```

The hub serves the bundle same-origin and a client runtime imports it into the host page. Each scan also mirrors into the hub's messages feed — a summary plus one per rule.

A host can also mount the module itself — e.g. a Vite host via `/@fs/`:

```ts
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'
Expand Down
9 changes: 3 additions & 6 deletions examples/a11y-messages-playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,12 @@ The window is split in two:
## How it's wired

`src/a11y-messages-playground.ts` is the entire host-framework integration - a ~120-line Vite plugin
that runs `@devframes/hub` in the dev server, mounts the two devframes as docks,
and attaches the a11y page script as the a11y dock's `clientScript`:
that runs `@devframes/hub` in the dev server and mounts the two devframes as docks
(the a11y inspector declares its own page script):

```ts
a11yMessagesPlayground({
devframes: [a11yDevframe, messagesDevframe],
clientScripts: {
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
},
})
```

Expand All @@ -79,7 +76,7 @@ the focused dock - the same path a manual dock click takes.
| File | Role |
|---|---|
| `src/a11y-messages-playground.ts` | The Vite host - hub context, static + connection-meta mounts, side-car WS, instance-registry registration |
| `vite.config.ts` | Mounts a11y + messages; attaches the a11y page script as its dock's `clientScript` |
| `vite.config.ts` | Mounts a11y + messages |
| `src/client/main.ts` | Boots the client runtime, renders the dock rail + iframe stage |
| `src/client/app-under-test.ts` | The intentionally-broken, multi-route app the page script scans |
| `src/client/icons.ts` | Offline Phosphor icons for the dock rail |
Expand Down
19 changes: 4 additions & 15 deletions examples/a11y-messages-playground/src/a11y-messages-playground.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { HubInstance } from '@devframes/hub/initiate'
import type { ClientScriptEntry } from '@devframes/hub/types'
import type { DevframeDefinition } from 'devframe'
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
import { Server as NodeHttpServer } from 'node:http'
Expand All @@ -14,22 +13,15 @@ export interface A11yMessagesPlaygroundOptions {
port?: number
/** Devframes to mount as docks (here: a11y + messages). */
devframes?: DevframeDefinition[]
/**
* Per-dock client scripts, keyed by devframe id. Attached to the mounted
* iframe dock so the hub client runtime imports them into the host page -
* this is how the a11y inspector's in-page agent gets into the page it scans.
*/
clientScripts?: Record<string, ClientScriptEntry>
}

/**
* A tiny Vite plugin that runs `@devframes/hub` inside the Vite dev server -
* the same shape as `examples/hub-vite`, trimmed to the two plugins this
* playground pairs (a11y + messages). One `initHub()` call assembles the whole
* hub: it mounts each devframe as a dock (attaching the a11y agent as its
* client script), shares the WebSocket with Vite's own server, serves the
* discovery endpoints, and registers the playground in the global instance
* registry.
* hub: it mounts each devframe as a dock, shares the WebSocket with Vite's own
* server, serves the discovery endpoints, and registers the playground in the
* global instance registry.
*/
export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions = {}): Plugin {
const base = normalizeBase(options.base ?? '/__hub/')
Expand Down Expand Up @@ -70,10 +62,7 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
return join(cwd, 'node_modules/.a11y-messages-playground')
return join(homedir(), '.a11y-messages-playground')
},
devframes: (options.devframes ?? []).map((def) => {
const clientScript = options.clientScripts?.[def.id]
return clientScript ? { devframe: def, dock: { clientScript } } : def
}),
devframes: options.devframes ?? [],
// List the playground alongside standalone devframes in discovery
// tooling (`devframe connect`, the inspector's Instances tab).
register: {
Expand Down
8 changes: 1 addition & 7 deletions examples/a11y-messages-playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'
import createA11yDevframe from '@devframes/plugin-a11y'
import createMessagesDevframe from '@devframes/plugin-messages'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
Expand All @@ -16,12 +16,6 @@ export default defineConfig({
UnoCSS(),
a11yMessagesPlayground({
devframes: [a11yDevframe, messagesDevframe],
// Attach the a11y page script as the a11y dock's client script - served
// over Vite's `/@fs/` so it shares this page's origin (the in-page
// channel the page script and panel talk over is same-origin).
clientScripts: {
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
},
}),
],
})
4 changes: 2 additions & 2 deletions examples/hub-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it

Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle - the choice rides a `?transport=` URL param and reconnects the whole client runtime on the pinned transport.

The A11y Inspector shows a live axe-core report of this hub's own page: the hub serves the devframe's page-script module (`a11yPageScriptBundlePath`) same-origin inside the hub namespace and attaches it as the a11y dock's `clientScript` (the `{ devframe, dock }` entry form); the hub client runtime - `createDevframeClientRuntime()` booted in `app/page.tsx` - imports it into the page, so the docked panel and the page script share the origin and tab their in-page channel handshakes across.
The A11y Inspector shows a live axe-core report of this hub's own page: the devframe declares its own page-script module as the a11y dock's `clientScript`, so the hub serves it same-origin with no host wiring; the hub client runtime - `createDevframeClientRuntime()` booted in `app/page.tsx` - imports it into the page, so the docked panel and the page script share the origin and tab their in-page channel handshakes across.

The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The hub registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (e.g. `pnpm --filter hub-vite dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA.

Expand Down Expand Up @@ -57,7 +57,7 @@ The built-in devframes run node-side (child processes, the native `zigpty` PTY b

| File | Role |
|---|---|
| `src/client/devframe/next-devframe-hub.ts` | The Next host - one `initHub()` call: devframes (incl. the a11y page script as its dock's `clientScript`), hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration |
| `src/client/devframe/next-devframe-hub.ts` | The Next host - one `initHub()` call: devframes, hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration |
| `src/client/devframe/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness |
| `../demo-dock-client/` | The shared demo client script, consumed here as a statically-mounted self-contained bundle |
| `src/client/app/%5F_devframes/[[...path]]/route.ts` | The one catch-all - delegates every `/__devframes/*` request to the instance's `handler` |
Expand Down
54 changes: 1 addition & 53 deletions examples/hub-next/src/client/devframe/next-devframe-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,44 +88,6 @@ async function loadJsonRenderUiRenderer(): Promise<DockRendererRegistration> {
return (mod.jsonRenderUiRenderer as typeof JsonRenderUiRenderer)()
}

/**
* URL base the a11y agent module is served under - inside the hub namespace,
* so the one catch-all route reaches it.
*/
const A11Y_AGENT_MOUNT_BASE = `${DEVFRAMES_HUB_BASE}df-a11y-agent/`

interface A11yAgentMount {
/** The a11y devframe's dock id - the dock the client script attaches to. */
dockId: string
/** On-disk directory holding the built agent module. */
dir: string
/** Same-origin URL of the agent module, importable by the hub client runtime. */
importFrom: string
}

/**
* Locate the a11y inspector's in-page **agent** module so the hub can serve it
* same-origin and attach it to the a11y dock as its client script - the hub
* client runtime (booted in `app/page.tsx`) imports it into the host page,
* where it scans this hub live. Loaded through the same bundler-ignored dynamic
* `import()` as the plugins, since the package resolves its `dist` via
* `import.meta.url`. Returns `null` if unavailable.
*/
async function loadA11yAgentMount(): Promise<A11yAgentMount | null> {
try {
const mod = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-a11y')
const bundle = mod.a11yPageScriptBundlePath as string
return {
dockId: (mod.default as () => DevframeDefinition)().id,
dir: dirname(bundle),
importFrom: `${A11Y_AGENT_MOUNT_BASE}inject.js`,
}
}
catch {
return null
}
}

/**
* URL base the demo dock-client bundle is served under - inside the hub
* namespace, so the one catch-all route reaches it.
Expand Down Expand Up @@ -215,13 +177,6 @@ export async function nextDevframeHub(
const nextPort = Number(process.env.PORT ?? 3000)
const origin = `http://${hostName}:${nextPort}`

// Serve the a11y inspector's in-page agent same-origin (inside the hub
// namespace, via the catch-all route) and attach it to the a11y dock as its
// client script. The hub client runtime booted in `app/page.tsx` imports it
// into the host page, where it scans this hub live; the panel iframe shares
// the origin, so their BroadcastChannel connects.
const a11yAgent = await loadA11yAgentMount()

// The shared demo dock-client script, served as a prebuilt self-contained
// bundle (see loadDemoDockClientMount above for why this host uses the
// URL shape rather than a bare specifier).
Expand All @@ -238,11 +193,7 @@ export async function nextDevframeHub(
// shim reports - all sharing one iframe.
const devframes: (DevframeDefinition | HubDevframeEntry)[] = [
demoDevframe,
...(await loadBuiltinPlugins()).map<DevframeDefinition | HubDevframeEntry>(def =>
a11yAgent && def.id === a11yAgent.dockId
? { devframe: def, dock: { clientScript: { importFrom: a11yAgent.importFrom } } }
: def,
),
...await loadBuiltinPlugins(),
await loadDataInspectorDevframe(),
await loadAssetsDevframe(),
{
Expand Down Expand Up @@ -327,9 +278,6 @@ export async function nextDevframeHub(
category: '~builtin',
})

if (a11yAgent)
await ctx.host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir)

// The demo dock-client script - the same package the Vite reference
// host loads via a bare specifier - mounted statically and attached as
// a momentary `action` dock by its served URL.
Expand Down
4 changes: 2 additions & 2 deletions examples/hub-vite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it

Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle - the choice rides a `?transport=` URL param and reconnects the whole client runtime on the pinned transport.

The A11y Inspector shows a live axe-core report of this hub's own page. `vite.config.ts` attaches the devframe's page script as the a11y dock's `clientScript` (served via `/@fs/`), and the hub client runtime - `createDevframeClientRuntime()` booted in `src/client/main.ts` - imports it into the host page. Panel and page script share the Vite origin and tab their in-page channel handshakes across; hover a violation to ring the offending element in the hub UI.
The A11y Inspector shows a live axe-core report of this hub's own page. The devframe declares its own page script as the a11y dock's `clientScript`, so the hub serves it same-origin and the hub client runtime - `createDevframeClientRuntime()` booted in `src/client/main.ts` - imports it into the host page automatically (no wiring in `vite.config.ts`). Panel and page script share the Vite origin and tab their in-page channel handshakes across; hover a violation to ring the offending element in the hub UI.

The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The hub registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (`pnpm --filter a11y-messages-playground dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA.

Expand All @@ -45,7 +45,7 @@ The dock UI is plain DOM in `src/client/`. To skin your own hub UI provider, rea
| File | Role |
|---|---|
| `src/vite-devframe-hub.ts` | The Vite host - one `initHub()` call mounted as connect middleware, plus instance-registry registration |
| `vite.config.ts` | Passes the built-in and demo devframes to the hub's `devframes` option; attaches the a11y page script as its dock's `clientScript`; composes the json-render frontend via `renderers` |
| `vite.config.ts` | Passes the built-in and demo devframes to the hub's `devframes` option; composes the json-render frontend via `renderers` |
| `src/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness |
| `../demo-dock-client/` | The shared demo client script, consumed here via bare specifier (`action: { importFrom: 'demo-dock-client' }`) |
| `src/client/main.ts` | The browser UI that consumes the hub protocol, including the interactive-OTP authorization view |
Expand Down
9 changes: 1 addition & 8 deletions examples/hub-vite/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DevframeHubContext } from '@devframes/hub/node'
import { defineHubRpcFunction } from '@devframes/hub'
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'
import createA11yDevframe from '@devframes/plugin-a11y'
import createAssetsDevframe from '@devframes/plugin-assets'
import createCodeServerDevframe from '@devframes/plugin-code-server'
import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector'
Expand Down Expand Up @@ -142,13 +142,6 @@ export default defineConfig({
},
},
],
// Attach the a11y inspector's in-page agent as its dock's client script.
// The hub client runtime (booted in src/client/main.ts) imports it into
// this page so the docked panel scans the host live - no bespoke
// injection plugin needed. `/@fs/` lets Vite serve the built module.
clientScripts: {
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
},
// Serve the reference json-render frontend as a prebuilt renderer
// module: the hub publishes it in the renderer manifest and the client
// (src/client/main.ts) imports it lazily the first time a
Expand Down
1 change: 1 addition & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
// deliberately invisible to bundlers (and knip) so Next never inlines
// their node-only code. They're genuinely used at runtime.
"ignoreDependencies": [
"@devframes/plugin-a11y",
"@devframes/plugin-code-server",
"@devframes/plugin-git",
"@devframes/plugin-inspect",
Expand Down
15 changes: 15 additions & 0 deletions packages/devframe/src/types/devframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ export interface DevframeDockDefaults {
badge?: string
/** Id of the dock group this entry collapses under, if any. */
groupId?: string
/**
* A client script the hub imports into the host page (this devframe's **page
* script**). An absolute-path `importFrom` is served by the hub under the
* mount base and rewritten to that URL, so mounting by package name needs no
* host wiring; a URL or bare specifier passes through untouched.
*/
clientScript?: {
/** An absolute filesystem path, a served URL, or a bare npm specifier. */
importFrom: string
/**
* The name to import the module as.
* @default 'default'
*/
importName?: string
}
}

/**
Expand Down
Loading
Loading