Skip to content

Commit 5b43bcd

Browse files
committed
feat(hub): serve a definition's absolute-path page script automatically
Let a devframe declare its page script on `dock.clientScript.importFrom` as an absolute filesystem path. The hub install path mounts the file's directory under the devframe's mount base and rewrites `importFrom` to the served URL, so mounting the a11y inspector by package name works on every host with no per-host wiring. The a11y inspector now declares its own page script, and the reference hosts (hub-vite, hub-next, a11y-messages-playground) drop their manual mount + `clientScript` recipes. Closes #310
1 parent d25c4b5 commit 5b43bcd

16 files changed

Lines changed: 204 additions & 120 deletions

File tree

docs/content/1.guide/17.client-context.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ A failed import retries on the next dock update.
7676

7777
### Shipping a client script
7878

79-
`importFrom` accepts two shapes:
79+
`importFrom` accepts three shapes:
8080

81+
- **An absolute filesystem path** to a built, self-contained ES module — declared on the definition's `dock.clientScript`. The hub install path serves the file's directory under the devframe's mount base (`<base>__page-script/`) and rewrites `importFrom` to that served URL, so a definition names its own client script and mounting by package name works with no host wiring (this is how the [a11y inspector](/plugins/a11y) ships its page script).
8182
- **A URL served by the host framework** — a self-contained ES module; works on every host framework.
8283
- **A bare npm specifier** (`'vite-plugin-vue-tracer/client/vite-devtools'`) — resolved through the host framework.
8384

84-
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.
85+
A definition declares its own by path on `dock: { clientScript: { importFrom } }`. 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.
8586

8687
### Bare npm specifiers
8788

docs/content/5.plugins/4.a11y.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ The page script and the panel talk over the [in-page channel](/guide/in-page-cha
3131

3232
## In a hub
3333

34-
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:
34+
The definition declares the page script as its dock [client script](/guide/client-context), so mounting by package name works everywhere with no extra wiring:
35+
36+
```ts
37+
initHub({ devframes: ['@devframes/plugin-a11y'] })
38+
```
39+
40+
The hub serves the page-script bundle same-origin under the a11y mount base and rewrites the dock's `clientScript` to that URL; a client runtime (`createDevframeClientRuntime()`) then imports it into the host page. Each scan also mirrors into the hub's messages feed — a summary plus one per rule.
41+
42+
A host that mounts the module itself still can — for example, a Vite host serving it via `/@fs/` — by attaching `a11yPageScriptBundlePath` as the dock's `clientScript`:
3543

3644
```ts
3745
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'

examples/a11y-messages-playground/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,13 @@ The window is split in two:
5555
## How it's wired
5656

5757
`src/a11y-messages-playground.ts` is the entire host-framework integration - a ~120-line Vite plugin
58-
that runs `@devframes/hub` in the dev server, mounts the two devframes as docks,
59-
and attaches the a11y page script as the a11y dock's `clientScript`:
58+
that runs `@devframes/hub` in the dev server and mounts the two devframes as docks.
59+
The a11y inspector declares its own page script as the a11y dock's `clientScript`, so
60+
the hub serves it same-origin and boots it - the host passes only the devframes:
6061

6162
```ts
6263
a11yMessagesPlayground({
6364
devframes: [a11yDevframe, messagesDevframe],
64-
clientScripts: {
65-
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
66-
},
6765
})
6866
```
6967

@@ -79,7 +77,7 @@ the focused dock - the same path a manual dock click takes.
7977
| File | Role |
8078
|---|---|
8179
| `src/a11y-messages-playground.ts` | The Vite host - hub context, static + connection-meta mounts, side-car WS, instance-registry registration |
82-
| `vite.config.ts` | Mounts a11y + messages; attaches the a11y page script as its dock's `clientScript` |
80+
| `vite.config.ts` | Mounts a11y + messages (the a11y page script rides along as its dock's declared `clientScript`) |
8381
| `src/client/main.ts` | Boots the client runtime, renders the dock rail + iframe stage |
8482
| `src/client/app-under-test.ts` | The intentionally-broken, multi-route app the page script scans |
8583
| `src/client/icons.ts` | Offline Phosphor icons for the dock rail |

examples/a11y-messages-playground/src/a11y-messages-playground.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { HubInstance } from '@devframes/hub/initiate'
2-
import type { ClientScriptEntry } from '@devframes/hub/types'
32
import type { DevframeDefinition } from 'devframe'
43
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
54
import { Server as NodeHttpServer } from 'node:http'
@@ -14,22 +13,17 @@ export interface A11yMessagesPlaygroundOptions {
1413
port?: number
1514
/** Devframes to mount as docks (here: a11y + messages). */
1615
devframes?: DevframeDefinition[]
17-
/**
18-
* Per-dock client scripts, keyed by devframe id. Attached to the mounted
19-
* iframe dock so the hub client runtime imports them into the host page -
20-
* this is how the a11y inspector's in-page agent gets into the page it scans.
21-
*/
22-
clientScripts?: Record<string, ClientScriptEntry>
2316
}
2417

2518
/**
2619
* A tiny Vite plugin that runs `@devframes/hub` inside the Vite dev server -
2720
* the same shape as `examples/hub-vite`, trimmed to the two plugins this
2821
* playground pairs (a11y + messages). One `initHub()` call assembles the whole
29-
* hub: it mounts each devframe as a dock (attaching the a11y agent as its
30-
* client script), shares the WebSocket with Vite's own server, serves the
31-
* discovery endpoints, and registers the playground in the global instance
32-
* registry.
22+
* hub: it mounts each devframe as a dock, shares the WebSocket with Vite's own
23+
* server, serves the discovery endpoints, and registers the playground in the
24+
* global instance registry. The a11y inspector declares its own page script
25+
* (its dock's `clientScript`), so the hub serves it same-origin and boots it -
26+
* no client-script wiring here.
3327
*/
3428
export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions = {}): Plugin {
3529
const base = normalizeBase(options.base ?? '/__hub/')
@@ -70,10 +64,7 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
7064
return join(cwd, 'node_modules/.a11y-messages-playground')
7165
return join(homedir(), '.a11y-messages-playground')
7266
},
73-
devframes: (options.devframes ?? []).map((def) => {
74-
const clientScript = options.clientScripts?.[def.id]
75-
return clientScript ? { devframe: def, dock: { clientScript } } : def
76-
}),
67+
devframes: options.devframes ?? [],
7768
// List the playground alongside standalone devframes in discovery
7869
// tooling (`devframe connect`, the inspector's Instances tab).
7970
register: {

examples/a11y-messages-playground/vite.config.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'
1+
import createA11yDevframe from '@devframes/plugin-a11y'
22
import createMessagesDevframe from '@devframes/plugin-messages'
33
import UnoCSS from 'unocss/vite'
44
import { defineConfig } from 'vite'
@@ -14,14 +14,11 @@ export default defineConfig({
1414
optimizeDeps: { exclude: ['@antfu/design'] },
1515
plugins: [
1616
UnoCSS(),
17+
// The a11y inspector ships its own page script (its dock's `clientScript`),
18+
// so the hub serves it same-origin and boots it - the panel scans this page
19+
// live with no wiring here.
1720
a11yMessagesPlayground({
1821
devframes: [a11yDevframe, messagesDevframe],
19-
// Attach the a11y page script as the a11y dock's client script - served
20-
// over Vite's `/@fs/` so it shares this page's origin (the in-page
21-
// channel the page script and panel talk over is same-origin).
22-
clientScripts: {
23-
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
24-
},
2522
}),
2623
],
2724
})

examples/hub-next/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it
2020

2121
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.
2222

23-
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.
23+
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 inside the hub namespace and rewrites the entry to that URL 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.
2424

2525
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.
2626

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

5858
| File | Role |
5959
|---|---|
60-
| `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 |
60+
| `src/client/devframe/next-devframe-hub.ts` | The Next host - one `initHub()` call: devframes (the a11y page script rides along automatically as its dock's declared `clientScript`), hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration |
6161
| `src/client/devframe/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness |
6262
| `../demo-dock-client/` | The shared demo client script, consumed here as a statically-mounted self-contained bundle |
6363
| `src/client/app/%5F_devframes/[[...path]]/route.ts` | The one catch-all - delegates every `/__devframes/*` request to the instance's `handler` |

examples/hub-next/src/client/devframe/next-devframe-hub.ts

Lines changed: 10 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -88,44 +88,6 @@ async function loadJsonRenderUiRenderer(): Promise<DockRendererRegistration> {
8888
return (mod.jsonRenderUiRenderer as typeof JsonRenderUiRenderer)()
8989
}
9090

91-
/**
92-
* URL base the a11y agent module is served under - inside the hub namespace,
93-
* so the one catch-all route reaches it.
94-
*/
95-
const A11Y_AGENT_MOUNT_BASE = `${DEVFRAMES_HUB_BASE}df-a11y-agent/`
96-
97-
interface A11yAgentMount {
98-
/** The a11y devframe's dock id - the dock the client script attaches to. */
99-
dockId: string
100-
/** On-disk directory holding the built agent module. */
101-
dir: string
102-
/** Same-origin URL of the agent module, importable by the hub client runtime. */
103-
importFrom: string
104-
}
105-
106-
/**
107-
* Locate the a11y inspector's in-page **agent** module so the hub can serve it
108-
* same-origin and attach it to the a11y dock as its client script - the hub
109-
* client runtime (booted in `app/page.tsx`) imports it into the host page,
110-
* where it scans this hub live. Loaded through the same bundler-ignored dynamic
111-
* `import()` as the plugins, since the package resolves its `dist` via
112-
* `import.meta.url`. Returns `null` if unavailable.
113-
*/
114-
async function loadA11yAgentMount(): Promise<A11yAgentMount | null> {
115-
try {
116-
const mod = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-a11y')
117-
const bundle = mod.a11yPageScriptBundlePath as string
118-
return {
119-
dockId: (mod.default as () => DevframeDefinition)().id,
120-
dir: dirname(bundle),
121-
importFrom: `${A11Y_AGENT_MOUNT_BASE}inject.js`,
122-
}
123-
}
124-
catch {
125-
return null
126-
}
127-
}
128-
12991
/**
13092
* URL base the demo dock-client bundle is served under - inside the hub
13193
* namespace, so the one catch-all route reaches it.
@@ -215,13 +177,6 @@ export async function nextDevframeHub(
215177
const nextPort = Number(process.env.PORT ?? 3000)
216178
const origin = `http://${hostName}:${nextPort}`
217179

218-
// Serve the a11y inspector's in-page agent same-origin (inside the hub
219-
// namespace, via the catch-all route) and attach it to the a11y dock as its
220-
// client script. The hub client runtime booted in `app/page.tsx` imports it
221-
// into the host page, where it scans this hub live; the panel iframe shares
222-
// the origin, so their BroadcastChannel connects.
223-
const a11yAgent = await loadA11yAgentMount()
224-
225180
// The shared demo dock-client script, served as a prebuilt self-contained
226181
// bundle (see loadDemoDockClientMount above for why this host uses the
227182
// URL shape rather than a bare specifier).
@@ -231,18 +186,18 @@ export async function nextDevframeHub(
231186
// way so its `import.meta.url` bundle path resolves to the published `dist`.
232187
const jsonRenderRenderer = await loadJsonRenderUiRenderer()
233188

234-
// Demo devframes alongside the dogfooded built-in plugin packages. The
235-
// shared-iframe soft-navigation demo mounts as a `subTabs` anchor (a shared
236-
// `frameId` + the postmessage protocol) so the client host attaches the
237-
// frame-nav adapter, materializing one client-only dock per tab the SPA's
238-
// shim reports - all sharing one iframe.
189+
// Demo devframes alongside the dogfooded built-in plugin packages. The a11y
190+
// inspector declares its own page script (its dock's `clientScript`) by path,
191+
// so the hub serves it same-origin and the hub client runtime booted in
192+
// `app/page.tsx` imports it into the host page automatically - it scans this
193+
// hub live with no host-side wiring. The shared-iframe soft-navigation demo
194+
// mounts as a `subTabs` anchor (a shared `frameId` + the postmessage
195+
// protocol) so the client host attaches the frame-nav adapter, materializing
196+
// one client-only dock per tab the SPA's shim reports - all sharing one
197+
// iframe.
239198
const devframes: (DevframeDefinition | HubDevframeEntry)[] = [
240199
demoDevframe,
241-
...(await loadBuiltinPlugins()).map<DevframeDefinition | HubDevframeEntry>(def =>
242-
a11yAgent && def.id === a11yAgent.dockId
243-
? { devframe: def, dock: { clientScript: { importFrom: a11yAgent.importFrom } } }
244-
: def,
245-
),
200+
...await loadBuiltinPlugins(),
246201
await loadDataInspectorDevframe(),
247202
await loadAssetsDevframe(),
248203
{
@@ -327,9 +282,6 @@ export async function nextDevframeHub(
327282
category: '~builtin',
328283
})
329284

330-
if (a11yAgent)
331-
await ctx.host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir)
332-
333285
// The demo dock-client script - the same package the Vite reference
334286
// host loads via a bare specifier - mounted statically and attached as
335287
// a momentary `action` dock by its served URL.

examples/hub-vite/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it
2020

2121
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.
2222

23-
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.
23+
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.
2424

2525
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.
2626

@@ -45,7 +45,7 @@ The dock UI is plain DOM in `src/client/`. To skin your own hub UI provider, rea
4545
| File | Role |
4646
|---|---|
4747
| `src/vite-devframe-hub.ts` | The Vite host - one `initHub()` call mounted as connect middleware, plus instance-registry registration |
48-
| `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` |
48+
| `vite.config.ts` | Passes the built-in and demo devframes to the hub's `devframes` option (the a11y page script rides along automatically as its dock's declared `clientScript`); composes the json-render frontend via `renderers` |
4949
| `src/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness |
5050
| `../demo-dock-client/` | The shared demo client script, consumed here via bare specifier (`action: { importFrom: 'demo-dock-client' }`) |
5151
| `src/client/main.ts` | The browser UI that consumes the hub protocol, including the interactive-OTP authorization view |

examples/hub-vite/vite.config.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DevframeHubContext } from '@devframes/hub/node'
22
import { defineHubRpcFunction } from '@devframes/hub'
33
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
44
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
5-
import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y'
5+
import createA11yDevframe from '@devframes/plugin-a11y'
66
import createAssetsDevframe from '@devframes/plugin-assets'
77
import createCodeServerDevframe from '@devframes/plugin-code-server'
88
import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector'
@@ -142,13 +142,11 @@ export default defineConfig({
142142
},
143143
},
144144
],
145-
// Attach the a11y inspector's in-page agent as its dock's client script.
146-
// The hub client runtime (booted in src/client/main.ts) imports it into
147-
// this page so the docked panel scans the host live - no bespoke
148-
// injection plugin needed. `/@fs/` lets Vite serve the built module.
149-
clientScripts: {
150-
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
151-
},
145+
// The a11y inspector declares its own page script (its dock's
146+
// `clientScript`) by path, so the hub serves it same-origin and the hub
147+
// client runtime (booted in src/client/main.ts) imports it into this page
148+
// automatically - the docked panel scans the host live with no host-side
149+
// wiring.
152150
// Serve the reference json-render frontend as a prebuilt renderer
153151
// module: the hub publishes it in the renderer manifest and the client
154152
// (src/client/main.ts) imports it lazily the first time a

knip.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
// deliberately invisible to bundlers (and knip) so Next never inlines
6767
// their node-only code. They're genuinely used at runtime.
6868
"ignoreDependencies": [
69+
"@devframes/plugin-a11y",
6970
"@devframes/plugin-code-server",
7071
"@devframes/plugin-git",
7172
"@devframes/plugin-inspect",

0 commit comments

Comments
 (0)