Skip to content

Commit ec4aa61

Browse files
authored
feat(hub): serve a definition's absolute-path page script automatically (#312)
1 parent f1bd9b8 commit ec4aa61

16 files changed

Lines changed: 152 additions & 117 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
@@ -55,12 +55,13 @@ A failed import retries on the next dock update.
5555

5656
### Shipping a client script
5757

58-
`importFrom` accepts two shapes:
58+
`importFrom` accepts three shapes:
5959

6060
- **A URL served by the host framework** — a self-contained ES module; works on every host framework.
6161
- **A bare npm specifier** (`'vite-plugin-vue-tracer/client/vite-devtools'`) — resolved through the host framework.
62+
- **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.
6263

63-
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.
64+
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.
6465

6566
### Bare npm specifiers
6667

‎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 just works:
35+
36+
```ts
37+
initHub({ devframes: ['@devframes/plugin-a11y'] })
38+
```
39+
40+
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.
41+
42+
A host can also mount the module itself — e.g. a Vite host via `/@fs/`:
3543

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

‎examples/a11y-messages-playground/README.md‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,12 @@ 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):
6060

6161
```ts
6262
a11yMessagesPlayground({
6363
devframes: [a11yDevframe, messagesDevframe],
64-
clientScripts: {
65-
[a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` },
66-
},
6764
})
6865
```
6966

@@ -79,7 +76,7 @@ the focused dock - the same path a manual dock click takes.
7976
| File | Role |
8077
|---|---|
8178
| `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` |
79+
| `vite.config.ts` | Mounts a11y + messages |
8380
| `src/client/main.ts` | Boots the client runtime, renders the dock rail + iframe stage |
8481
| `src/client/app-under-test.ts` | The intentionally-broken, multi-route app the page script scans |
8582
| `src/client/icons.ts` | Offline Phosphor icons for the dock rail |

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

Lines changed: 4 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,15 @@ 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.
3325
*/
3426
export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions = {}): Plugin {
3527
const base = normalizeBase(options.base ?? '/__hub/')
@@ -70,10 +62,7 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions =
7062
return join(cwd, 'node_modules/.a11y-messages-playground')
7163
return join(homedir(), '.a11y-messages-playground')
7264
},
73-
devframes: (options.devframes ?? []).map((def) => {
74-
const clientScript = options.clientScripts?.[def.id]
75-
return clientScript ? { devframe: def, dock: { clientScript } } : def
76-
}),
65+
devframes: options.devframes ?? [],
7766
// List the playground alongside standalone devframes in discovery
7867
// tooling (`devframe connect`, the inspector's Instances tab).
7968
register: {

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

Lines changed: 1 addition & 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'
@@ -16,12 +16,6 @@ export default defineConfig({
1616
UnoCSS(),
1717
a11yMessagesPlayground({
1818
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-
},
2519
}),
2620
],
2721
})

‎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 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, 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: 1 addition & 53 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).
@@ -238,11 +193,7 @@ export async function nextDevframeHub(
238193
// shim reports - all sharing one iframe.
239194
const devframes: (DevframeDefinition | HubDevframeEntry)[] = [
240195
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-
),
196+
...await loadBuiltinPlugins(),
246197
await loadDataInspectorDevframe(),
247198
await loadAssetsDevframe(),
248199
{
@@ -327,9 +278,6 @@ export async function nextDevframeHub(
327278
category: '~builtin',
328279
})
329280

330-
if (a11yAgent)
331-
await ctx.host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir)
332-
333281
// The demo dock-client script - the same package the Vite reference
334282
// host loads via a bare specifier - mounted statically and attached as
335283
// 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; 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: 1 addition & 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,6 @@ 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-
},
152145
// Serve the reference json-render frontend as a prebuilt renderer
153146
// module: the hub publishes it in the renderer manifest and the client
154147
// (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)