Skip to content

Commit 6d3b3c8

Browse files
committed
refactor(devframe,hub): plain ctx.staticConfig + split DevframeDockPreferences
Applies review feedback: - `ctx.configs` (the `DevframeConfigsHost` with `contribute`/`resolve`) is now `ctx.staticConfig` — a plain, non-reactive `Partial<DevframeConnectionConfigsRegistry>` object mutated at setup time. Drops the host class and its test; the instance shell serializes the object straight into `ConnectionMeta.configs`. - The hub-wide dock-bar preferences (`categoryOrder` / `maxVisibleItems` / `defaultMode` / `defaultPosition`) move off the per-entry `DevframeDockDefaults` into a dedicated `DevframeDockPreferences` type, declared on the new `DevframeDefinition.dockPreferences`. `installDevframe` aggregates it into `ctx.staticConfig.dock`; the hub's `configs.dock` registry key now points at `DevframeDockPreferences` (the `DevframeDockConfig` alias is gone). Built with the help of an agent.
1 parent 0ba639e commit 6d3b3c8

16 files changed

Lines changed: 95 additions & 195 deletions

File tree

‎docs/guide/devframe-definition.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ interface DevframeNodeContext {
113113
diagnostics: DevframeDiagnosticsHost
114114
agent: DevframeAgentHost // experimental
115115
services: DevframeServicesHost // typed cross-plugin service registry
116-
configs: DevframeConfigsHost // contribute to this context's own ConnectionMeta.configs
116+
staticConfig: Partial<DevframeConnectionConfigsRegistry> // this context's own ConnectionMeta.configs
117117

118118
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
119119
}
@@ -133,7 +133,7 @@ ctx.services.whenAvailable('my-plugin:sources', (sources) => {
133133

134134
### Static connection configs
135135

136-
`ctx.configs` builds up `ConnectionMeta.configs` — static, boot-time data delivered once through the connection handshake every client already performs, and never mutated again. Contrast it with `ctx.scope(id).settings`, which is mutable and synced bidirectionally over shared-state RPC for the life of the session.
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.
137137

138138
```ts
139139
declare module 'devframe/types' {
@@ -142,7 +142,7 @@ declare module 'devframe/types' {
142142
}
143143
}
144144

145-
ctx.configs.contribute('my-plugin', () => ({ featureFlag: true }))
145+
ctx.staticConfig['my-plugin'] = { featureFlag: true }
146146
```
147147

148148
`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.

‎packages/devframe/src/node/__tests__/host-configs.test.ts‎

Lines changed: 0 additions & 39 deletions
This file was deleted.

‎packages/devframe/src/node/context.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { DevframeHost, DevframeNodeContext, DevframeScopedNodeContext } fro
33
import { diagnostics as rpcDiagnostics } from '../rpc/diagnostics'
44
import { diagnostics as devframeDiagnostics } from './diagnostics'
55
import { DevframeAgentHost } from './host-agent'
6-
import { DevframeConfigsHostImpl } from './host-configs'
76
import { DevframeDiagnosticsHost } from './host-diagnostics'
87
import { RpcFunctionsHostImpl } from './host-functions'
98
import { DevframeServicesHostImpl } from './host-services'
@@ -45,7 +44,7 @@ export async function createHostContext(options: CreateHostContextOptions): Prom
4544
diagnostics: undefined!,
4645
agent: undefined!,
4746
services: undefined!,
48-
configs: undefined!,
47+
staticConfig: {},
4948
scope: undefined!,
5049
} as unknown as DevframeNodeContext
5150

@@ -56,7 +55,6 @@ export async function createHostContext(options: CreateHostContextOptions): Prom
5655
context.views = viewsHost
5756
context.diagnostics = diagnosticsHost
5857
context.services = new DevframeServicesHostImpl()
59-
context.configs = new DevframeConfigsHostImpl()
6058

6159
// Agent host must be constructed after `rpcHost` so it can subscribe
6260
// to `onChanged` — it auto-discovers RPC functions flagged with

‎packages/devframe/src/node/host-configs.ts‎

Lines changed: 0 additions & 22 deletions
This file was deleted.

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

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

592-
// Every contribution (`ctx.configs.contribute(...)`) made during
593-
// `options.init(api)` — e.g. a hub aggregating each installed
594-
// devframe's own dock-bar preferences — is in by now; bake the
595-
// resolved configs into the meta `options.mount` (and every host that
596-
// re-serves this same meta at another base) publishes.
597-
const configs = ctx.configs.resolve()
598-
if (Object.keys(configs).length > 0)
599-
meta.configs = configs
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
600599

601600
await options.mount?.(ctx, meta, api)
602601

‎packages/devframe/src/types/context.ts‎

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,21 @@ export interface DevframeNodeContext {
6565
*/
6666
services: DevframeServicesHost
6767
/**
68-
* The API for contributing to this context's own {@link ConnectionMeta.configs}
69-
* — static, boot-time config a host publishes once and never mutates
70-
* again. See {@link DevframeConfigsHost}.
68+
* This context's own {@link ConnectionMeta.configs} — static, boot-time
69+
* config a host publishes once through the connection handshake and every
70+
* client reads read-only. A plain, **non-reactive** object: mutate it
71+
* during `setup(ctx)` (a plugin sets its keys, a hub aggregates across
72+
* every installed devframe), never during the session — it's serialized
73+
* once, after setup, and changing it afterwards reaches no client.
74+
*
75+
* ```ts
76+
* ctx.staticConfig.dock = {
77+
* ...ctx.staticConfig.dock,
78+
* categoryOrder: { ...ctx.staticConfig.dock?.categoryOrder, ...myOrder },
79+
* }
80+
* ```
7181
*/
72-
// TODO: call it `ctx.staticConfig`, and have it a plain object. No need for complex host here.
73-
// Just mention that it's not reactive and should be updated only at setup time, not during the session.
74-
configs: DevframeConfigsHost
82+
staticConfig: Partial<DevframeConnectionConfigsRegistry>
7583
/**
7684
* Create a namespace-scoped view of this context. The returned
7785
* `ctx.scope('my-plugin')` auto-namespaces every RPC id, shared-state
@@ -246,30 +254,3 @@ export interface ConnectionMeta {
246254
* augments it with `dock`; `@devframes/hub-ui` augments it with `ui`.
247255
*/
248256
export interface DevframeConnectionConfigsRegistry {}
249-
250-
/**
251-
* The API for building up {@link ConnectionMeta.configs} — every registered
252-
* key merges from whatever contributes to it (a plugin's own `setup(ctx)`,
253-
* a hub aggregating across every installed devframe, …) into the one
254-
* document a host publishes once and serves for the life of the server.
255-
*
256-
* `updater` receives whatever's been contributed to `key` so far (or
257-
* `undefined` on the first contribution) and returns the new value — the
258-
* contributor owns its own merge semantics (overwrite, shallow-merge a
259-
* record, …), not this host.
260-
*
261-
* ```ts
262-
* ctx.configs.contribute('dock', (current = {}) => ({
263-
* ...current,
264-
* categoryOrder: { ...current.categoryOrder, ...myCategoryOrder },
265-
* }))
266-
* ```
267-
*/
268-
export interface DevframeConfigsHost {
269-
contribute: <K extends keyof DevframeConnectionConfigsRegistry>(
270-
key: K,
271-
updater: (current: DevframeConnectionConfigsRegistry[K] | undefined) => DevframeConnectionConfigsRegistry[K],
272-
) => void
273-
/** Everything contributed so far — what a host publishes as `ConnectionMeta.configs`. */
274-
resolve: () => Partial<DevframeConnectionConfigsRegistry>
275-
}

‎packages/devframe/src/types/devframe.ts‎

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -262,30 +262,34 @@ export interface DevframeDockDefaults {
262262
badge?: string
263263
/** Id of the dock group this entry collapses under, if any. */
264264
groupId?: string
265-
// TODO: `categoryOrder` / `maxVisibleItems` / `defaultMode` / `defaultPosition` are not per-entry attributes, but global hub-wide preferences. They should be moved to a separate `DevframeDockPreferences` type and surfaced in the connection meta, not on the entry itself.
265+
}
266+
267+
/**
268+
* A devframe's opinion about the **hub-wide** dock bar — not attributes of
269+
* its own synthesized entry, but preferences the hub aggregates across every
270+
* installed devframe and publishes once via `ConnectionMeta.configs.dock`.
271+
* Declared on {@link DevframeDefinition.dockPreferences}. Standalone adapters
272+
* (`cli` / `spa` / `build`) ignore this entirely.
273+
*/
274+
export interface DevframeDockPreferences {
266275
/**
267-
* This devframe's opinion about the top-level dock-bar **category**
268-
* ordering, hub-wide — not an attribute of this entry itself. Every
269-
* installed devframe's `categoryOrder` is shallow-merged into one
270-
* aggregate (last-installed wins per key), delivered once via
271-
* `ConnectionMeta.configs.dock.categoryOrder` and merged beneath
272-
* `DEFAULT_CATEGORIES_ORDER`. A host page's own
276+
* The top-level dock-bar **category** ordering. Every installed devframe's
277+
* `categoryOrder` is shallow-merged into one aggregate (last-installed wins
278+
* per key) and merged beneath `DEFAULT_CATEGORIES_ORDER`. A host page's own
273279
* `createDevframeClientHost({ categoryOrder })` still overrides it.
274280
*/
275281
categoryOrder?: Record<string, number>
276282
/**
277-
* This devframe's preferred inline-item capacity for the floating dock
278-
* bar before entries overflow — hub-wide, not an attribute of this entry.
279-
* The last installed devframe declaring it wins; an explicit `layout`
280-
* prop passed to the dock UI still overrides it. Edge mode ignores this
281-
* by design — it shows every entry with no capacity cutoff.
283+
* Preferred inline-item capacity for the floating dock bar before entries
284+
* overflow. The last installed devframe declaring it wins; an explicit
285+
* `layout` prop passed to the dock UI still overrides it. Edge mode ignores
286+
* this by design — it shows every entry with no capacity cutoff.
282287
*/
283288
maxVisibleItems?: number
284289
/**
285-
* Seeds a first-run visitor's dock mode — hub-wide, not an attribute of
286-
* this entry. Only applies when the visitor has no stored dock
287-
* preference yet; never overwrites one who already moved their dock. The
288-
* last installed devframe declaring it wins.
290+
* Seeds a first-run visitor's dock mode. Only applies when the visitor has
291+
* no stored dock preference yet; never overwrites one who already moved
292+
* their dock. The last installed devframe declaring it wins.
289293
*/
290294
defaultMode?: 'float' | 'edge'
291295
/**
@@ -344,6 +348,15 @@ export interface DevframeDefinition {
344348
* @see {@link DevframeDockDefaults}
345349
*/
346350
dock?: DevframeDockDefaults
351+
/**
352+
* This devframe's opinion about the hub-wide dock bar (category ordering,
353+
* float-mode capacity, first-run mode/position). Consulted only by the hub
354+
* install path, which aggregates it across every installed devframe into
355+
* `ConnectionMeta.configs.dock`; standalone adapters ignore it.
356+
*
357+
* @see {@link DevframeDockPreferences}
358+
*/
359+
dockPreferences?: DevframeDockPreferences
347360
/**
348361
* Mount path override. Defaults depend on the adapter:
349362
* `/` for standalone (`cli` / `spa` / `build`), `/__<id>/` for hosted

‎packages/hub/src/node/__tests__/initiate.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ describe('initHub', () => {
178178
const wsPort = await getPort({ port: 18225, host: '127.0.0.1' })
179179
const alpha: DevframeDefinition = {
180180
...makeFrame('alpha', makeDist('<!doctype html><title>frame a</title>')),
181-
dock: { category: 'app', categoryOrder: { app: -40 }, maxVisibleItems: 4 },
181+
dock: { category: 'app' },
182+
dockPreferences: { categoryOrder: { app: -40 }, maxVisibleItems: 4 },
182183
}
183184
const beta: DevframeDefinition = {
184185
...makeFrame('beta'),
185-
dock: { category: 'web', categoryOrder: { app: -60, web: 300 }, defaultMode: 'edge' },
186+
dock: { category: 'web' },
187+
dockPreferences: { categoryOrder: { app: -60, web: 300 }, defaultMode: 'edge' },
186188
}
187189

188190
const hub = initHub({

‎packages/hub/src/node/__tests__/install-devframe.test.ts‎

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DevframeConfigsHost, DevframeDefinition, DevframeDuplicationStrategy } from 'devframe/types'
1+
import type { DevframeDefinition, DevframeDuplicationStrategy } from 'devframe/types'
22
import type { DevframeHubContext } from '../context'
33
import { mkdtempSync } from 'node:fs'
44
import { tmpdir } from 'node:os'
@@ -8,17 +8,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
88
import { DevframeDocksHost } from '../host-docks'
99
import { installDevframe } from '../install-devframe'
1010

11-
/** Minimal stand-in for `createHostContext`'s real `DevframeConfigsHostImpl`. */
12-
function createConfigsHost(): DevframeConfigsHost {
13-
const values: Record<string, any> = {}
14-
return {
15-
contribute: (key, updater) => {
16-
values[key] = updater(values[key])
17-
},
18-
resolve: () => values,
19-
}
20-
}
21-
2211
function createContext(): DevframeHubContext {
2312
const storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-install-'))
2413
const context = {
@@ -30,7 +19,8 @@ function createContext(): DevframeHubContext {
3019
views: {
3120
hostStatic: () => {},
3221
},
33-
configs: createConfigsHost(),
22+
// `createHostContext` seeds this plain object; the fake mirrors it.
23+
staticConfig: {},
3424
} as unknown as DevframeHubContext
3525
context.docks = new DevframeDocksHost(context)
3626
// `createHubContext` wires this; the hand-built fake context here does the
@@ -109,19 +99,19 @@ describe('ctx.install', () => {
10999
})
110100
})
111101

112-
it('contributes categoryOrder/maxVisibleItems/defaultMode/defaultPosition to ctx.configs instead of the entry', async () => {
102+
it('writes dockPreferences to ctx.staticConfig.dock, not the synthesized entry', async () => {
113103
const ctx = createContext()
114104
await ctx.install(makeDevframe({
115-
dock: {
116-
category: 'app',
105+
dock: { category: 'app' },
106+
dockPreferences: {
117107
categoryOrder: { app: -40 },
118108
maxVisibleItems: 4,
119109
defaultMode: 'edge',
120110
defaultPosition: 'left',
121111
},
122112
}))
123113

124-
expect(ctx.configs.resolve().dock).toEqual({
114+
expect(ctx.staticConfig.dock).toEqual({
125115
categoryOrder: { app: -40 },
126116
maxVisibleItems: 4,
127117
defaultMode: 'edge',
@@ -136,23 +126,23 @@ describe('ctx.install', () => {
136126
expect(entry.category).toBe('app')
137127
})
138128

139-
it('leaves ctx.configs untouched when a devframe declares no dock-bar preferences', async () => {
129+
it('leaves ctx.staticConfig untouched when a devframe declares no dock preferences', async () => {
140130
const ctx = createContext()
141131
await ctx.install(makeDevframe({ dock: { category: 'app' } }))
142-
expect(ctx.configs.resolve().dock).toBeUndefined()
132+
expect(ctx.staticConfig.dock).toBeUndefined()
143133
})
144134

145135
it('shallow-merges categoryOrder across two devframes, last wins per scalar field', async () => {
146136
const ctx = createContext()
147137
await ctx.install(makeDevframe({
148-
dock: { categoryOrder: { app: -40, web: 300 }, maxVisibleItems: 4, defaultMode: 'edge' },
138+
dockPreferences: { categoryOrder: { app: -40, web: 300 }, maxVisibleItems: 4, defaultMode: 'edge' },
149139
}))
150140
await ctx.install(makeDevframe({
151141
id: 'demo-2',
152-
dock: { categoryOrder: { app: -60, advanced: -50 }, maxVisibleItems: 10 },
142+
dockPreferences: { categoryOrder: { app: -60, advanced: -50 }, maxVisibleItems: 10 },
153143
}))
154144

155-
expect(ctx.configs.resolve().dock).toEqual({
145+
expect(ctx.staticConfig.dock).toEqual({
156146
categoryOrder: { app: -60, web: 300, advanced: -50 },
157147
maxVisibleItems: 10,
158148
defaultMode: 'edge',

‎packages/hub/src/node/initiate.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export function initHub(options: InitHubOptions): HubInstance {
534534

535535
mount(ctx, meta) {
536536
// `meta.configs` already carries whatever every installed devframe
537-
// contributed via `ctx.configs` (e.g. aggregated dock-bar preferences
537+
// wrote to `ctx.staticConfig` (e.g. aggregated dock-bar preferences
538538
// under `dock`) — add the UI slot's own opaque config (e.g. branding)
539539
// under `ui`, without interpreting it, staying policy-free about what
540540
// "ui" means.

0 commit comments

Comments
 (0)