Skip to content

Commit 4e08b14

Browse files
antfubotantfu
andauthored
feat(devframe,hub,hub-ui): static connection-meta configs + plugin-declared dock layout (#227)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 59eef9b commit 4e08b14

42 files changed

Lines changed: 603 additions & 231 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/guide/build-your-own-hub-ui.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface DevframeHubUi {
1616
viewer?: { distDir: string } // a standalone SPA served at the hub base
1717
embedded?: { entry: string } // a self-contained bootstrap at <base>embedded.js
1818
assets?: Record<string, () => string | Uint8Array> // extra UI-owned files
19+
setup?: (ctx) => void | Promise<void> // publish static config via ctx.staticConfig
1920
}
2021
```
2122

@@ -24,6 +25,14 @@ prebuilt assets: the viewer SPA is built with relative asset paths, and the
2425
embedded entry is one self-contained ES module that mounts your dock into any
2526
host page.
2627

28+
`setup(ctx)` runs once during hub init — write your static, boot-time config
29+
to `ctx.staticConfig`, which is serialized into `ConnectionMeta.configs` and
30+
read by the client from the one connection handshake it already performs. The
31+
reference UI's `createUi({ branding })` uses it to set
32+
`ctx.staticConfig.ui = { branding, … }`; the hub never interprets what you
33+
write. It's the structured, read-only counterpart to `assets` (arbitrary
34+
served files).
35+
2736
## The client contracts
2837

2938
A viewer renders from the hub's shared state and drives it through

docs/guide/devframe-definition.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ interface DevframeNodeContext {
113113
diagnostics: DevframeDiagnosticsHost
114114
agent: DevframeAgentHost // expose tools + resources to coding agents
115115
services: DevframeServicesHost // typed cross-plugin service registry
116+
staticConfig: Partial<DevframeConnectionConfigsRegistry> // this context's own ConnectionMeta.configs
116117

117118
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
118119
}
@@ -130,6 +131,22 @@ ctx.services.whenAvailable('my-plugin:sources', (sources) => {
130131
})
131132
```
132133

134+
### Static connection configs
135+
136+
`ctx.staticConfig` is this context's own `ConnectionMeta.configs` — static, boot-time data delivered once through the connection handshake every client already performs, and read-only from the browser. It's a plain, **non-reactive** object: write it during `setup(ctx)`, never during the session (it's serialized once, after setup). Contrast it with `ctx.scope(id).settings`, which is mutable and synced bidirectionally over shared-state RPC for the life of the session.
137+
138+
```ts
139+
declare module 'devframe/types' {
140+
interface DevframeConnectionConfigsRegistry {
141+
'my-plugin': { featureFlag: boolean }
142+
}
143+
}
144+
145+
ctx.staticConfig['my-plugin'] = { featureFlag: true }
146+
```
147+
148+
`updater` receives whatever's been contributed to that key so far (or `undefined` on the first contribution), so multiple contributors sharing a key — a hub aggregating each installed devframe's own preference, for example — own their own merge semantics (overwrite, shallow-merge a record, …) rather than the host imposing one.
149+
133150
### Storage scopes
134151

135152
`ctx.host.getStorageDir(scope)` places persisted state in one of three classes:
@@ -152,6 +169,7 @@ Each devframe-level host has a dedicated page:
152169
- [Shared State](./shared-state)`ctx.rpc.sharedState`
153170
- [Diagnostics](./diagnostics)`ctx.diagnostics`
154171
- [Agent-Native](./agent-native)`ctx.agent`
172+
- [Cross-Plugin Services](./services)`ctx.services`
155173

156174
## Browser setup
157175

docs/guide/hub-initiate.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,27 @@ The hub is headless — `DevframeHubUi` is pure data, and whoever fills it decid
5656
interface DevframeHubUi {
5757
viewer?: { distDir: string } // a standalone SPA served at the namespace root
5858
embedded?: { entry: string } // a prebuilt bootstrap served at <base>embedded.js
59+
assets?: Record<string, () => string | Uint8Array> // extra UI-owned files
60+
setup?: (ctx) => void | Promise<void> // publish static config via ctx.staticConfig
5961
}
6062
```
6163

62-
`@devframes/hub-ui`'s `createUi()` is the reference implementation: a standalone viewer plus the floating dock — one `<script type="module" src="/__devframes/embedded.js">` tag in the host page and the dock mounts itself, always visible. A viewer product supplies a different object to the same slot and reuses all the infrastructure; visibility policy (keyboard summon, passive modes) belongs entirely to the entry's author.
64+
`@devframes/hub-ui`'s `createUi()` is the reference implementation: a standalone viewer plus the floating dock — one `<script type="module" src="/__devframes/embedded.js">` tag in the host page and the dock mounts itself. A viewer product supplies a different object to the same slot and reuses all the infrastructure. Its `setup(ctx)` publishes the reference UI's config to `ctx.staticConfig.ui`, which rides `ConnectionMeta.configs.ui` to the client.
65+
66+
`createUi()` takes a few options:
67+
68+
- **`branding`** — rebrand the reference UI (logo, product name, primary color).
69+
- **`dockPreferences`** — dock-bar rendering: `categoryOrder`, floating-dock `maxVisibleItems`, and the first-run `defaultMode` (`'float'` / `'edge'`) and `defaultPosition`.
70+
- **`embeddedVisibility`** — the floating dock's reveal policy:
71+
- `'normal'` (default) — the dock is shown immediately.
72+
- `'passive'` — the dock starts hidden with a console hint; `Shift+Alt+D` reveals it, and the reveal persists per-origin so later sessions start shown.
73+
- `'hidden'` — the dock starts hidden; `Shift+Alt+D` reveals it for the current session only.
74+
75+
```ts
76+
createUi({ embeddedVisibility: 'passive', dockPreferences: { defaultMode: 'edge' } })
77+
```
78+
79+
Each seeds a user-overridable preference — the config sets the default, the visitor's own choice (reveal/hide, float/edge, …) wins from then on.
6380

6481
## Renderer modules
6582

examples/hub-hono-minimal/src/app.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ export const hub: HubInstance = globalRef.__hubHonoMinimal ??= initHub({
3939
createAssetsDevframe({ watch: false }),
4040
],
4141
// Rebrand the reference UI to Hono's own orange — one field, no CSS:
42-
// `createUi`'s `branding` option publishes `branding.json`, which the dock
43-
// fetches at boot and feeds into `--devframe-primary` (see
44-
// `@devframes/hub-ui`'s `primary-ramp.css`).
42+
// `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
43+
// which the dock reads at connect time and feeds into `--devframe-primary`
44+
// (see `@devframes/hub-ui`'s `primary-ramp.css`).
4545
ui: createUi({ branding: { primaryColor: '#e36002', productName: 'Devframes on Hono' } }),
4646
// Gate with devframe's interactive OTP (the default). The hub prints a
4747
// 6-digit code + magic link on startup, and the reference UI's authorization

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ async function loadHub(): Promise<HubInstance> {
7373
// `@devframes/next/hub` runs the socket on a side-car (Next routes can't
7474
// accept WS upgrades). This host overrides the default UI slot to rebrand
7575
// the reference viewer to Next.js/Vercel's monochrome black — one field, no
76-
// CSS: `createUi`'s `branding` option publishes `branding.json`, which the
77-
// dock fetches at boot and feeds into `--devframe-primary` (see
78-
// `@devframes/hub-ui`'s `primary-ramp.css`).
76+
// CSS: `createUi`'s `branding` option publishes
77+
// `ConnectionMeta.configs.ui.branding`, which the dock reads at connect
78+
// time and feeds into `--devframe-primary` (see `@devframes/hub-ui`'s
79+
// `primary-ramp.css`).
7980
return createNextDevframeHub({
8081
devframes,
81-
ui: (hubUi.createUi as typeof CreateUi)({ branding: { primaryColor: '#000000', productName: 'Devframes on Next.js' } }),
82+
ui: (hubUi.createUi as typeof CreateUi)({ branding: { primaryColor: '#3f8ba9', productName: 'Devframes on Next.js' } }),
8283
// Serve the reference json-render frontend as a prebuilt renderer module
8384
// — the one-liner that makes `'json-render'` docks render in the prebuilt
8485
// viewer. Swap it for any community implementation of the same contract.

examples/hub-nitro-minimal/hub.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export const hub: HubInstance = globalRef.__hubNitroMinimal ??= initHub({
4242
createAssetsDevframe({ watch: false }),
4343
],
4444
// Rebrand the reference UI to Nitro's own pink/red — one field, no CSS:
45-
// `createUi`'s `branding` option publishes `branding.json`, which the dock
46-
// fetches at boot and feeds into `--devframe-primary` (see
47-
// `@devframes/hub-ui`'s `primary-ramp.css`).
45+
// `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
46+
// which the dock reads at connect time and feeds into `--devframe-primary`
47+
// (see `@devframes/hub-ui`'s `primary-ramp.css`).
4848
ui: createUi({ branding: { primaryColor: '#ff2056', productName: 'Devframes on Nitro' } }),
4949
// Gate with devframe's interactive OTP (the default). The hub prints a
5050
// 6-digit code + magic link on startup, and the reference UI's authorization

examples/hub-rsbuild-minimal/rsbuild.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ export default defineConfig({
7777
base,
7878
devframes: builtinDevframes,
7979
// Rebrand the reference UI to Rsbuild's own orange — one field, no
80-
// CSS: `createUi`'s `branding` option publishes `branding.json`,
81-
// which the dock fetches at boot and feeds into `--devframe-primary`
82-
// (see `@devframes/hub-ui`'s `primary-ramp.css`).
80+
// CSS: `createUi`'s `branding` option publishes
81+
// `ConnectionMeta.configs.ui.branding`, which the dock reads at
82+
// connect time and feeds into `--devframe-primary` (see
83+
// `@devframes/hub-ui`'s `primary-ramp.css`).
8384
ui: createUi({ branding: { primaryColor: '#ff5e00', productName: 'Devframes on Rsbuild' } }),
8485
// Serve the reference json-render frontend as a prebuilt renderer
8586
// module — the one-liner that makes `'json-render'` docks render in

examples/hub-vite-minimal/vite.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ export default defineConfig({
6767
quiet: true,
6868
devframes: builtinDevframes,
6969
// Rebrand the reference UI to Vite's own purple — one field, no CSS:
70-
// `createUi`'s `branding` option publishes `branding.json`, which the
71-
// dock fetches at boot and feeds into `--devframe-primary` (see
72-
// `@devframes/hub-ui`'s `primary-ramp.css`). Passing `ui` overrides the
73-
// default `createUi()` the plugin would otherwise use.
70+
// `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
71+
// which the dock reads at connect time and feeds into `--devframe-primary`
72+
// (see `@devframes/hub-ui`'s `primary-ramp.css`). Passing `ui` overrides
73+
// the default `createUi()` the plugin would otherwise use.
7474
ui: createUi({ branding: { primaryColor: '#646cff', productName: 'Devframes on Vite' } }),
7575
// Serve the reference json-render frontend as a prebuilt renderer
7676
// module — the one-liner that makes `'json-render'` docks render in

packages/devframe/src/node/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function createHostContext(options: CreateHostContextOptions): Prom
4444
diagnostics: undefined!,
4545
agent: undefined!,
4646
services: undefined!,
47+
staticConfig: {},
4748
scope: undefined!,
4849
} as unknown as DevframeNodeContext
4950

packages/devframe/src/node/instance-shell.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,14 @@ export function createInstanceShell<TContext extends DevframeNodeContext>(
589589
...(result.mcp ? { mcp: result.mcp } : {}),
590590
}
591591

592+
// Whatever `setup(ctx)` wrote to `ctx.staticConfig` during
593+
// `options.init(api)` — e.g. a hub aggregating each installed devframe's
594+
// own dock-bar preferences — is in by now; bake it into the meta
595+
// `options.mount` (and every host that re-serves this same meta at
596+
// another base) publishes.
597+
if (Object.keys(ctx.staticConfig).length > 0)
598+
meta.configs = ctx.staticConfig
599+
592600
await options.mount?.(ctx, meta, api)
593601

594602
// A pinned origin means the banner and registry record needn't wait for a

0 commit comments

Comments
 (0)