Skip to content

Commit 46e5bd8

Browse files
committed
feat(data-inspector): example source, docs page, in-app docs links
- built-in example source: a small live playground graph (Maps, Sets, class instances, circulars, BigInt, a query-time getter) with suggested queries; on by default, disabled via exampleSource: false, suppressed automatically by the agent and by the CLI when data files are given - docs/plugins/data-inspector.md: using the plugin, providing data sources (registry + context service + contract), CLI, agent attach, static export, RPC surface; registered in the plugins sidebar/overview - the SPA header links to the docs page, and an empty-source state points at the providing-data-sources section
1 parent c3f7c4e commit 46e5bd8

13 files changed

Lines changed: 300 additions & 7 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function helpersItems(prefix: string) {
6161
function pluginsItems(prefix: string) {
6262
return [
6363
{ text: 'Overview', link: `${prefix}/plugins/` },
64+
{ text: 'Data Inspector', link: `${prefix}/plugins/data-inspector` },
6465
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
6566
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
6667
{ text: 'Git', link: `${prefix}/plugins/git` },

docs/plugins/data-inspector.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Data Inspector
6+
7+
An interactive query workbench for live server-side objects, built as a **Vue** SPA. Plugins and hosts register **data sources**; you compose [jora](https://discoveryjs.github.io/jora/) queries against them — executed in the process that owns the objects — and explore normalized results in a struct view with type badges, a data-shape panel, filters, and saved queries.
8+
9+
Package: `@devframes/plugin-data-inspector` · framework: **Vue + Vite**
10+
11+
## What it does
12+
13+
- **Query workbench** — a CodeMirror jora editor with syntax highlighting and server-computed autocomplete; queries auto-run as you type, with a client-side syntax gate so malformed input never hits the wire. Source, query, and filters persist in the URL, so any workbench state is shareable.
14+
- **Result viewer** — results normalize to strict JSON (circulars become `$ref` markers; Maps, Sets, class instances, functions, and Dates get type badges) with per-query stats: jora / normalize / rpc timings, payload size, node count. The value-actions popup copies paths and turns any key into a query.
15+
- **Data shape panel** — a one-level type skeleton of the active source, independent of the query; click a property to query it.
16+
- **Filters** — exclude functions, `_`-prefixed, or `$`-prefixed properties from results and skeleton alike.
17+
- **Saved queries** — recipes (`query` + optional title/description + the filters they were authored with), id-keyed, in two scopes: **workspace** (committable, shared with the team) and **project** (per-checkout).
18+
19+
A built-in example source demonstrates all of it on first run; disable it with `createDataInspectorDevframe({ exampleSource: false })` once your own sources are registered.
20+
21+
## Providing data sources
22+
23+
The registry is **process-global**: register from anywhere in the process — plugin setup, host hooks, application code — before or after the inspector mounts.
24+
25+
```ts
26+
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
27+
28+
registerDataSource({
29+
id: 'my-plugin:store', // namespace with your plugin id
30+
title: 'My plugin store',
31+
description: 'The live state store',
32+
icon: 'i-ph:database-duotone',
33+
data: () => store,
34+
queries: [
35+
{ title: 'Active sessions', query: 'sessions.mapEntries().value.[active]' },
36+
{ title: 'Config (data only)', query: 'config', excludeFunctions: true },
37+
],
38+
})
39+
```
40+
41+
The contract:
42+
43+
```ts
44+
interface DataSourceEntry {
45+
id: string
46+
title: string
47+
description?: string
48+
icon?: string
49+
/** A plain value, or a factory returning one (sync or async). */
50+
data: unknown | (() => unknown | Promise<unknown>)
51+
/** The resolved value never changes: resolve once and memoize. */
52+
static?: boolean
53+
/** Suggested queries, shown read-only next to saved ones. */
54+
queries?: Query[]
55+
}
56+
```
57+
58+
Live objects passed to `data` stay live — every query reads their current state. `registerDataSource` returns an unregister function, and connected workbenches refresh their source list on every change.
59+
60+
Integrations that prefer zero package dependency consume the same registry through the typed [context service](../guide/devframe-definition#cross-plugin-services):
61+
62+
```ts
63+
ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
64+
sources.register({ id: 'my-plugin:store', title: 'My store', data: () => store })
65+
})
66+
```
67+
68+
> [!WARNING]
69+
> Queries are eval-grade access to registered objects: jora can invoke any function reachable as an own property and fires own getters. Register live objects with that in mind, and keep inspector endpoints on loopback.
70+
71+
## Mount into a Vite host
72+
73+
```ts
74+
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
75+
// vite.config.ts
76+
import { dataInspectorVitePlugin } from '@devframes/plugin-data-inspector/vite'
77+
import { defineConfig } from 'vite'
78+
79+
export default defineConfig({
80+
plugins: [
81+
dataInspectorVitePlugin(),
82+
{
83+
name: 'my-app:data-sources',
84+
configureServer(server) {
85+
registerDataSource({
86+
id: 'vite:server',
87+
title: 'Vite Dev Server',
88+
data: () => server,
89+
queries: [{ title: 'Plugin names', query: 'config.plugins.name' }],
90+
})
91+
},
92+
},
93+
],
94+
})
95+
```
96+
97+
Hub hosts mount the default export like any devframe definition.
98+
99+
## Standalone CLI
100+
101+
```sh
102+
devframe-data-inspector # the example source
103+
devframe-data-inspector stats.json log.jsonl # one static source per data file
104+
devframe-data-inspector build stats.json # self-contained static export
105+
devframe-data-inspector attach # attach to a process running the agent
106+
```
107+
108+
`.json` files parse whole; `.jsonl` / `.ndjson` parse as an array of records. `build` writes a static site embedding the dataset — the same query engine runs client-side there, so saved recipes keep working.
109+
110+
## Attach to another Node process
111+
112+
The target process opts in by starting the agent:
113+
114+
```ts
115+
import { exposeDataInspector } from '@devframes/plugin-data-inspector/agent'
116+
117+
await exposeDataInspector()
118+
```
119+
120+
or with zero code changes:
121+
122+
```sh
123+
DEVFRAME_DATA_INSPECTOR=1 node --import @devframes/plugin-data-inspector/agent server.js
124+
```
125+
126+
The agent binds `127.0.0.1`, requires devframe's trust handshake with a per-run pre-shared token, and advertises its endpoint in `node_modules/.data-inspector/agent.json``devframe-data-inspector attach` consumes it automatically (or pass `ws://…` and `--token` explicitly). Queries execute inside the target process, where the live objects are. Treat the endpoint like a debugger port.
127+
128+
## RPC surface
129+
130+
All functions are namespaced `devframes:plugin:data-inspector:*`:
131+
132+
| Function | Type | Returns |
133+
|----------|------|---------|
134+
| `sources` | `query` | Every registered source (meta and suggested queries). |
135+
| `query` | `query` | Runs a jora query against a source; normalized result with stats. |
136+
| `skeleton` | `query` | The type skeleton of a source, honoring the filter options. |
137+
| `suggest` | `query` | Autocomplete candidates from jora's stat mode at a cursor position. |
138+
| `saved:list` / `saved:save` / `saved:delete` | `query` / `action` | Saved-query recipes in the `workspace` and `project` scopes. |

docs/plugins/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf
1010

1111
| Plugin | UI framework | What it does |
1212
|--------|--------------|--------------|
13+
| [Data Inspector](./data-inspector) | Vue | Query live server-side objects with jora — sources contributed by plugins, hosts, data files, or attached processes. |
1314
| [Devframe Inspector](./inspect) | Vue | Browse the RPC registry, invoke read-only queries, watch shared state update live, and explore the agent surface. |
1415
| [Accessibility Inspector](./a11y) | Solid | Run axe-core against a host app, list WCAG violations, and highlight the offending element in the page on hover. |
1516
| [Git](./git) | React (Next.js) | A repository dashboard — status, a commit graph, branches, and diffs, with optional staging and committing. |
@@ -18,7 +19,7 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf
1819

1920
## One client, any framework
2021

21-
The five plugins span Vue, Solid, React, Svelte, and framework-free TypeScript, yet they share the same node-side surface: register RPC functions, publish shared state, and connect from the browser with `connectDevframe`. Whatever renders the UI — a reactive framework or a handful of DOM calls — talks to the backend through the same protocol.
22+
The collection spans Vue, Solid, React, Svelte, and framework-free TypeScript, yet every plugin shares the same node-side surface: register RPC functions, publish shared state, and connect from the browser with `connectDevframe`. Whatever renders the UI — a reactive framework or a handful of DOM calls — talks to the backend through the same protocol.
2223

2324
This is the framework-agnostic promise in practice. The browser bundle is the author's to choose; devframe handles the transport, the data model, the adapters, and the agent surface underneath.
2425

plugins/data-inspector/src/agent/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ export async function exposeDataInspector(options: ExposeDataInspectorOptions =
9494
}
9595

9696
const context: DevframeNodeContext = await createHostContext({ cwd, mode: 'dev', host })
97-
setupDataInspector(context)
97+
// Attach exists to inspect the target's own objects; the example source
98+
// would only be noise next to them.
99+
setupDataInspector(context, { exampleSource: false })
98100

99101
const useAuth = options.auth ?? true
100102
const token = useAuth ? (options.token ?? randomToken()) : undefined

plugins/data-inspector/src/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export function createDataInspectorCli() {
5454
.action(async (files: string[], flags: { port?: number, host: string, open?: boolean }) => {
5555
for (const file of files)
5656
registerDataSource(createFileDataSource(file))
57-
const def = createDataInspectorDevframe()
57+
// With files given, they are the point; the example source only backs
58+
// the empty first-run.
59+
const def = createDataInspectorDevframe({ exampleSource: files.length === 0 })
5860
await createDevServer(def, {
5961
host: flags.host,
6062
port: flags.port ? Number(flags.port) : undefined,

plugins/data-inspector/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export interface DataInspectorDevframeOptions {
3636
* agent (`@devframes/plugin-data-inspector/agent`) defaults to `true`.
3737
*/
3838
auth?: boolean
39+
/**
40+
* Register the built-in example source — a small live playground graph
41+
* with suggested queries (default `true`). Disable once your own sources
42+
* cover the first-run experience.
43+
*/
44+
exampleSource?: boolean
3945
}
4046

4147
/**
@@ -71,7 +77,7 @@ export function createDataInspectorDevframe(options: DataInspectorDevframeOption
7177
spa: { loader: 'none' },
7278
dock: { category: '~builtin' },
7379
setup(ctx) {
74-
setupDataInspector(ctx)
80+
setupDataInspector(ctx, { exampleSource: options.exampleSource })
7581
},
7682
})
7783
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { DataSourceEntry } from '../registry/index'
2+
import process from 'node:process'
3+
4+
/** Id of the built-in example source (namespaced like every source id). */
5+
export const EXAMPLE_SOURCE_ID = 'devframes:plugin:data-inspector:example'
6+
7+
class RequestLog {
8+
readonly startedAt = new Date()
9+
private secret = 'own-fields-are-visible'
10+
entries = new Map<string, { path: string, hits: number }>([
11+
['home', { path: '/', hits: 41 }],
12+
['docs', { path: '/docs', hits: 7 }],
13+
['api', { path: '/api/data', hits: 23 }],
14+
])
15+
}
16+
17+
function createExampleData(): Record<string, unknown> {
18+
const log = new RequestLog()
19+
const node: Record<string, unknown> = { name: 'root' }
20+
node.self = node // circular structures render as $ref markers
21+
22+
const data: Record<string, unknown> = {
23+
server: {
24+
name: 'example',
25+
port: 5173,
26+
tags: new Set(['dev', 'local', 'example']),
27+
middlewares: [
28+
{ name: 'compression', handler: () => {} },
29+
{ name: 'static', handler: () => {} },
30+
],
31+
},
32+
requests: log,
33+
build: {
34+
hash: 'c3f7c4e',
35+
size: 1024n * 512n,
36+
modules: Array.from({ length: 300 }, (_, i) => ({
37+
id: `src/module-${i}.ts`,
38+
imports: i % 7,
39+
sizeKb: Math.round(Math.sin(i) * 40 + 50),
40+
})),
41+
},
42+
tree: node,
43+
process: { pid: process.pid, node: process.version, platform: process.platform },
44+
}
45+
// An own getter: reads at query time, so re-running shows the data is live.
46+
Object.defineProperty(data, 'queriedAt', {
47+
enumerable: true,
48+
get: () => new Date().toISOString(),
49+
})
50+
return data
51+
}
52+
53+
/**
54+
* The built-in example source — a small live graph exercising everything the
55+
* viewer can show (Maps, Sets, class instances, circulars, BigInt, functions,
56+
* a query-time getter), with suggested queries as starting points. Sources
57+
* registered by your plugins and hosts appear next to it; disable it via
58+
* `createDataInspectorDevframe({ exampleSource: false })`.
59+
*/
60+
export function createExampleDataSource(): DataSourceEntry {
61+
let data: Record<string, unknown> | undefined
62+
return {
63+
id: EXAMPLE_SOURCE_ID,
64+
title: 'Example data',
65+
description: 'A built-in playground graph. Register your own sources to inspect real objects.',
66+
icon: 'i-ph:flask-duotone',
67+
data: () => data ??= createExampleData(),
68+
queries: [
69+
{
70+
title: 'Busiest endpoints',
71+
description: 'Live Map entries, sorted',
72+
query: 'requests.entries.mapEntries().value.sort(hits desc)',
73+
},
74+
{
75+
title: 'Large modules as a table',
76+
description: 'Filter and project an array',
77+
query: 'build.modules.[sizeKb > 80].({ id, sizeKb })',
78+
},
79+
{
80+
title: 'Server config (data only)',
81+
description: 'Filters strip the middleware functions',
82+
query: 'server',
83+
excludeFunctions: true,
84+
},
85+
{ title: 'Everything', query: '' },
86+
],
87+
}
88+
}
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import type { DevframeNodeContext } from 'devframe/types'
2-
import { createDataSourcesService, DATA_SOURCES_SERVICE_ID, onDataSourcesChanged } from '../registry/index'
2+
import { createDataSourcesService, DATA_SOURCES_SERVICE_ID, getDataSource, onDataSourcesChanged, registerDataSource } from '../registry/index'
33
import { serverFunctions } from '../rpc/index'
4+
import { createExampleDataSource, EXAMPLE_SOURCE_ID } from './example-source'
45

56
/** Broadcast whenever the source registry changes (register/unregister). */
67
export const SOURCES_CHANGED_EVENT = 'devframes:plugin:data-inspector:sources:changed'
78

9+
export interface SetupDataInspectorOptions {
10+
/** Register the built-in example source (default `true`). */
11+
exampleSource?: boolean
12+
}
13+
814
/**
915
* Register the data-inspector's RPC functions on a devframe node context,
1016
* provide the source registry as a typed context service, and broadcast
@@ -13,7 +19,7 @@ export const SOURCES_CHANGED_EVENT = 'devframes:plugin:data-inspector:sources:ch
1319
* Called from the definition's `setup(ctx)` and reusable by host adapters
1420
* (the CLI and the in-process agent wire their own contexts through this).
1521
*/
16-
export function setupDataInspector(ctx: DevframeNodeContext): void {
22+
export function setupDataInspector(ctx: DevframeNodeContext, options: SetupDataInspectorOptions = {}): void {
1723
for (const fn of serverFunctions)
1824
ctx.rpc.register(fn)
1925

@@ -22,9 +28,13 @@ export function setupDataInspector(ctx: DevframeNodeContext): void {
2228
if (!ctx.services.has(DATA_SOURCES_SERVICE_ID))
2329
ctx.services.provide(DATA_SOURCES_SERVICE_ID, createDataSourcesService())
2430

31+
if ((options.exampleSource ?? true) && !getDataSource(EXAMPLE_SOURCE_ID))
32+
registerDataSource(createExampleDataSource())
33+
2534
onDataSourcesChanged(() => {
2635
ctx.rpc.broadcast(SOURCES_CHANGED_EVENT as never)
2736
})
2837
}
2938

39+
export { createExampleDataSource, EXAMPLE_SOURCE_ID }
3040
export { serverFunctions }

plugins/data-inspector/src/spa/App.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ function queryAppend(path: string): void {
100100
:color="connection.connected ? 100 : 200"
101101
/>
102102
<div class="flex-auto" />
103+
<a
104+
href="https://devfra.me/plugins/data-inspector"
105+
target="_blank"
106+
title="Data Inspector docs — using the plugin and providing data sources"
107+
class="flex items-center gap-1 text-xs color-muted hover:color-active"
108+
>
109+
<span class="i-ph:book-open-duotone text-base" />
110+
<span>Docs</span>
111+
</a>
103112
<ActionDarkToggle
104113
:color-scheme="colorScheme"
105114
@update:color-scheme="colorScheme = $event"
@@ -126,6 +135,17 @@ function queryAppend(path: string): void {
126135
</Button>
127136
</div>
128137

138+
<div v-if="connection.connected && !wb.sources.value.length" class="text-xs color-muted">
139+
No data sources registered yet. Call
140+
<code class="font-mono bg-secondary border border-base rounded px-1">registerDataSource()</code>
141+
from your plugin or host —
142+
<a
143+
href="https://devfra.me/plugins/data-inspector#providing-data-sources"
144+
target="_blank"
145+
class="color-active hover:underline"
146+
>see the docs</a>.
147+
</div>
148+
129149
<template v-if="showDataSourceDetails">
130150
<DataShapePanel
131151
:source="wb.activeSource.value"

plugins/data-inspector/test/registry.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,17 @@ describe('data source registry (process-global)', () => {
9494
expect(listDataSources()).toEqual([])
9595
})
9696
})
97+
98+
describe('example source', () => {
99+
it('registers a live, queryable playground with suggested queries', async () => {
100+
const { createExampleDataSource } = await import('../src/node/example-source')
101+
const entry = createExampleDataSource()
102+
registerDataSource(entry)
103+
const data = await resolveSourceData(getDataSource(entry.id)!)
104+
const { runQuery } = await import('../src/engine/query-engine')
105+
for (const recipe of entry.queries ?? []) {
106+
const out = runQuery(data, recipe.query.trim() || '$', recipe)
107+
expect(out.ok, `suggested query "${recipe.title}" must run`).toBe(true)
108+
}
109+
})
110+
})

0 commit comments

Comments
 (0)