Skip to content

Commit bd2d10f

Browse files
antfubotantfu
andauthored
feat: data-inspector plugin (jora live-query workbench), ctx.services, storage scopes (#101)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 1cf6f1a commit bd2d10f

82 files changed

Lines changed: 5534 additions & 31 deletions

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/inject': p('data-inspector/src/inject/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/.vitepress/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function helpersItems(prefix: string) {
6262
function pluginsItems(prefix: string) {
6363
return [
6464
{ text: 'Overview', link: `${prefix}/plugins/` },
65+
{ text: 'Data Inspector', link: `${prefix}/plugins/data-inspector` },
6566
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
6667
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
6768
{ 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** is always registered alongside your own: it exposes the devframe context (registered RPC functions, services, storage dirs), OS info, and live process stats — with query-time getters that change on every re-run — plus a small playground branch exercising every viewer capability. Opt out with `createDataInspectorDevframe({ exampleSource: false })` (CLI: `--no-example`, agent: `DEVFRAME_DATA_INSPECTOR_EXAMPLE=0`).
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/inject'
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/inject 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

‎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/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
],

‎plugins/data-inspector/README.md‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# @devframes/plugin-data-inspector
2+
3+
Inspect live server-side objects interactively. Other plugins and hosts register **data sources**; the workbench composes [jora](https://github.com/discoveryjs/jora) queries against them — executed in the process that owns the objects — and renders normalized results in a [discovery.js](https://github.com/discoveryjs/discovery) struct view with type badges, a shape panel, saved queries, and shareable URL state.
4+
5+
## Register a data source
6+
7+
The registry is **process-global** — register from anywhere, before or after the plugin mounts:
8+
9+
```ts
10+
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
11+
12+
registerDataSource({
13+
id: 'my-plugin:state',
14+
title: 'My plugin state',
15+
description: 'The live state store',
16+
data: () => store, // value or (async) factory; `static: true` memoizes
17+
queries: [{ title: 'Active entries', query: 'entries.mapEntries()' }],
18+
})
19+
```
20+
21+
Integrations that prefer zero package dependency consume the same store through the typed context service:
22+
23+
```ts
24+
ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
25+
sources.register({ id: 'my-plugin:state', title: 'My state', data: () => store })
26+
})
27+
```
28+
29+
## Mount
30+
31+
```ts
32+
// hub hosts mount the default export like any devframe:
33+
import dataInspectorDevframe from '@devframes/plugin-data-inspector'
34+
// Vite
35+
import { dataInspectorVitePlugin } from '@devframes/plugin-data-inspector/vite'
36+
```
37+
38+
## Standalone CLI
39+
40+
```sh
41+
devframe-data-inspector stats.json trace.jsonl # inspect local data files
42+
devframe-data-inspector build stats.json # self-contained static export
43+
devframe-data-inspector attach # attach to a process running the agent
44+
```
45+
46+
Static exports embed the dataset and run the same query engine client-side, so saved recipes stay portable.
47+
48+
## Attach to another Node process
49+
50+
```ts
51+
import { exposeDataInspector } from '@devframes/plugin-data-inspector/inject'
52+
53+
await exposeDataInspector()
54+
```
55+
56+
or with zero code changes:
57+
58+
```sh
59+
DEVFRAME_DATA_INSPECTOR=1 node --import @devframes/plugin-data-inspector/inject server.js
60+
```
61+
62+
The agent binds `127.0.0.1`, requires devframe's trust handshake with a per-run token by default, and advertises its endpoint in `node_modules/.data-inspector/agent.json`, which `devframe-data-inspector attach` picks up automatically.
63+
64+
> [!WARNING]
65+
> A connected inspector runs eval-grade jora queries against live objects: queries can invoke functions reachable as own properties and fire getters. Treat the agent endpoint like a debugger port — keep it on loopback and keep auth on.
66+
67+
## Saved queries
68+
69+
Recipes (`{ query, title?, description?, ...filters }`) persist id-keyed in two scopes: **workspace** (committable, `getStorageDir('workspace')/data-inspector/queries.json` — shared with the team) and **project** (per-checkout, under `node_modules`).

‎plugins/data-inspector/bin.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process'
3+
import { createDataInspectorCli } from './dist/cli.mjs'
4+
5+
async function main() {
6+
const cli = createDataInspectorCli()
7+
await cli.parse()
8+
}
9+
10+
main().catch((error) => {
11+
console.error(error)
12+
process.exit(1)
13+
})

0 commit comments

Comments
 (0)