Skip to content

Commit 1832a08

Browse files
committed
feat: data-inspector plugin, cross-plugin services, storage scopes
Promotes the data-inspector spike to a real plugin and lands the two core primitives it design-required: devframe core: - ctx.services — typed, namespaced cross-plugin service registry (provide/get/whenAvailable, augmentable DevframeServicesRegistry, DF0037 on duplicate providers); whenAvailable absorbs setup-order differences between provider and consumer - getStorageDir scopes become workspace/project/global: workspace is the committable .devframe/ dir (team-shared), project is the per-checkout node_modules dir (the old 'workspace'), global stays per-user; all hosts and call sites migrated (settings and dock prefs now persist under 'project') @devframes/plugin-data-inspector: - process-global source registry (globalThis Symbol store): sources are { id, title, description?, icon?, data: value | (async) factory, static?, queries? }; registerDataSource() needs no context, the same store is provided as the 'devframes:plugin:data-inspector:sources' context service, and changes broadcast to connected UIs - isomorphic engine (normalize/skeleton/jora bridges) — the Map/Set methods duck-type live collections AND their normalized tags, so saved queries stay portable between live and static modes - Vue + @antfu/design SPA with discovery's CodeMirror jora editor (remote stat-mode suggestions), struct viewer with type badges, shape panel, filters, per-source drafts, URL state, saved queries (workspace/project scopes via the new storage classes) - standalone CLI: inspect .json/.jsonl files, attach to a process running the in-process agent (loopback + pre-shared-token auth by default, discovery file handshake), and build self-contained static exports that run the engine client-side - unit tests for registry/engine/saved-queries/file loaders; core services tests; API snapshots updated The prototype example is replaced by the plugin; the minimal Vite hub dogfoods it with the live ViteDevServer registered as a source.
1 parent 7af5d63 commit 1832a08

107 files changed

Lines changed: 3040 additions & 1134 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.

alias.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,18 @@ export const alias = {
6262
'@devframes/plugin-terminals/vite': p('terminals/src/vite.ts'),
6363
'@devframes/plugin-terminals': p('terminals/src/index.ts'),
6464
'@devframes/plugin-git': p('git/src/index.ts'),
65+
'devframe/recipes/interactive-auth': r('devframe/src/recipes/interactive-auth.ts'),
6566
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
6667
'devframe/client': r('devframe/src/client/index.ts'),
6768
'devframe': r('devframe/src'),
69+
'@devframes/plugin-data-inspector/client': p('data-inspector/src/client/index.ts'),
70+
'@devframes/plugin-data-inspector/node': p('data-inspector/src/node/index.ts'),
71+
'@devframes/plugin-data-inspector/registry': p('data-inspector/src/registry/index.ts'),
72+
'@devframes/plugin-data-inspector/engine': p('data-inspector/src/engine/index.ts'),
73+
'@devframes/plugin-data-inspector/agent': p('data-inspector/src/agent/index.ts'),
74+
'@devframes/plugin-data-inspector/cli': p('data-inspector/src/cli.ts'),
75+
'@devframes/plugin-data-inspector/vite': p('data-inspector/src/vite.ts'),
76+
'@devframes/plugin-data-inspector': p('data-inspector/src/index.ts'),
6877
'@devframes/plugin-inspect/client': p('inspect/src/client/index.ts'),
6978
'@devframes/plugin-inspect/node': p('inspect/src/node/index.ts'),
7079
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),

docs/errors/DF0037.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0037: Duplicate Service Provider
6+
7+
## Message
8+
9+
> A service is already provided under "`{id}`".
10+
11+
## Cause
12+
13+
`ctx.services` holds exactly one provider per service id. A second `provide()` under the same id throws instead of silently replacing a service another integration may already hold a reference to.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — provided twice
19+
ctx.services.provide('my-plugin:sources', hostA)
20+
ctx.services.provide('my-plugin:sources', hostB)
21+
22+
// ✓ Good — revoke the previous provider first
23+
const revoke = ctx.services.provide('my-plugin:sources', hostA)
24+
revoke()
25+
ctx.services.provide('my-plugin:sources', hostB)
26+
```
27+
28+
## Fix
29+
30+
- Revoke the existing provider first — `provide()` returns a revoke function.
31+
- If the collision is between two unrelated integrations, namespace the id with your plugin id (`<plugin-id>:<service>`), the same rule RPC function names follow.
32+
- Guard idempotent setup paths with `ctx.services.has(id)`.
33+
34+
## Source
35+
36+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts)`provide()` throws this when the id is already taken.

docs/guide/devframe-definition.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,37 @@ interface DevframeNodeContext {
112112
views: DevframeViewHost // static file hosting (`hostStatic`)
113113
diagnostics: DevframeDiagnosticsHost
114114
agent: DevframeAgentHost // experimental
115+
services: DevframeServicesHost // typed cross-plugin service registry
115116

116117
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
117118
}
118119
```
119120

121+
### Cross-plugin services
122+
123+
`ctx.services` is a typed, namespaced registry through which one integration exposes a capability and others consume it without a hard package dependency. The provider augments the `DevframeServicesRegistry` interface (so consumers get full typing from a types-only import) and provides the implementation at setup time; consumers use `whenAvailable`, which absorbs setup-order differences:
124+
125+
```ts
126+
// provider
127+
declare module 'devframe' {
128+
interface DevframeServicesRegistry {
129+
'my-plugin:sources': SourcesService
130+
}
131+
}
132+
ctx.services.provide('my-plugin:sources', sources)
133+
134+
// consumer — types come from `import type`, no runtime dependency
135+
ctx.services.whenAvailable('my-plugin:sources', (sources) => {
136+
sources.register(/* ... */)
137+
})
138+
```
139+
140+
Service ids follow the RPC naming rule: prefix with the providing plugin's id. Duplicate ids throw [`DF0037`](https://devfra.me/errors/DF0037).
141+
142+
### Storage scopes
143+
144+
`ctx.host.getStorageDir(scope)` places persisted state in one of three classes: `'workspace'` (committable, shared with the team — conventionally `<workspaceRoot>/.devframe/`), `'project'` (per-checkout private, under `node_modules`), and `'global'` (per-user, under the home directory).
145+
120146
`ctx.scope(id)` returns a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel and adds a persisted top-level `settings` store. It's the recommended entry point from a single tool's setup code — see [Scoped Context](./scoped-context).
121147

122148
Host adapters can augment `ctx` with additional surfaces. For example, the [`vite` adapter](/adapters/vite) exposes Vite DevTools' dock, command, message, and terminal hosts via an optional `setup` hook on `createPluginFromDevframe` — consult the host's docs for those extras.

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ export async function minimalNextDevframeHub(
177177
return `http://${hostName}:3000`
178178
},
179179
getStorageDir(scope) {
180-
return scope === 'workspace'
181-
? join(cwd, 'node_modules/.minimal-next-devframe-hub')
182-
: join(homedir(), '.minimal-next-devframe-hub')
180+
if (scope === 'workspace')
181+
return join(cwd, '.devframe')
182+
if (scope === 'project')
183+
return join(cwd, 'node_modules/.minimal-next-devframe-hub')
184+
return join(homedir(), '.minimal-next-devframe-hub')
183185
},
184186
}
185187

examples/minimal-vite-devframe-hub/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@devframes/hub": "workspace:*",
1616
"@devframes/plugin-a11y": "workspace:*",
1717
"@devframes/plugin-code-server": "workspace:*",
18+
"@devframes/plugin-data-inspector": "workspace:*",
1819
"@devframes/plugin-git": "workspace:*",
1920
"@devframes/plugin-inspect": "workspace:*",
2021
"@devframes/plugin-messages": "workspace:*",

examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,11 @@ export function minimalViteDevframeHub(options: MinimalViteDevframeHubOptions =
115115
return resolved ? new URL(resolved).origin : 'http://localhost:5173'
116116
},
117117
getStorageDir(scope) {
118-
return scope === 'workspace'
119-
? join(cwd, 'node_modules/.minimal-vite-devframe-hub')
120-
: join(homedir(), '.minimal-vite-devframe-hub')
118+
if (scope === 'workspace')
119+
return join(cwd, '.devframe')
120+
if (scope === 'project')
121+
return join(cwd, 'node_modules/.minimal-vite-devframe-hub')
122+
return join(homedir(), '.minimal-vite-devframe-hub')
121123
},
122124
}
123125

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import a11yDevframe, { a11yAgentBundlePath } from '@devframes/plugin-a11y'
22
import codeServerDevframe from '@devframes/plugin-code-server'
3+
import dataInspectorDevframe from '@devframes/plugin-data-inspector'
4+
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
35
import gitDevframe from '@devframes/plugin-git'
46
import inspectDevframe from '@devframes/plugin-inspect'
57
import messagesDevframe from '@devframes/plugin-messages'
@@ -18,6 +20,34 @@ export default defineConfig({
1820
server: { allowedHosts: true, strictPort: false },
1921
plugins: [
2022
UnoCSS(),
23+
{
24+
// The host registers its own live objects as data-inspector sources —
25+
// the registry is process-global, so this works from any plugin hook.
26+
name: 'minimal-vite-devframe-hub:data-sources',
27+
configureServer(server) {
28+
registerDataSource({
29+
id: 'vite:server',
30+
title: 'Vite Dev Server',
31+
description: 'The live ViteDevServer instance serving this hub.',
32+
icon: 'i-ph:lightning-duotone',
33+
data: () => server,
34+
queries: [
35+
{ title: 'Plugin names', query: 'config.plugins.name' },
36+
{
37+
title: 'Module graph',
38+
description: 'Client-environment modules with their importers',
39+
query: 'environments.client.moduleGraph.idToModuleMap.mapEntries().value.({ url, type, importers: importers.fromSet().url })',
40+
},
41+
{
42+
title: 'Resolved config (clean)',
43+
query: 'config',
44+
excludeFunctions: true,
45+
excludeUnderscoreProps: true,
46+
},
47+
],
48+
})
49+
},
50+
},
2151
minimalViteDevframeHub({
2252
devframes: [
2353
demoDevframe,
@@ -28,6 +58,7 @@ export default defineConfig({
2858
terminalsDevframe,
2959
codeServerDevframe,
3060
inspectDevframe,
61+
dataInspectorDevframe,
3162
a11yDevframe,
3263
messagesDevframe,
3364
],

examples/prototype-data-inspector/.devframe/data-inspector/queries.json

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

examples/prototype-data-inspector/README.md

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

examples/prototype-data-inspector/index.html

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

0 commit comments

Comments
 (0)