Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e5d70e5
chore: spike throwaway data-viewer prototype (jora live-query + disco…
antfubot Jul 16, 2026
7f4e665
feat(prototype): data-inspector rework — Vue/@antfu/design workbench,…
antfubot Jul 16, 2026
36941c1
feat(prototype): skeleton panel, query settings, root view, rerun, fl…
antfubot Jul 16, 2026
cf529c3
feat(prototype): query recipes, suggested queries, save dialog, type …
antfubot Jul 16, 2026
ce4234d
feat: adjust UI
antfu Jul 16, 2026
8007183
feat(prototype): implement UI TODOs — per-source drafts, source info …
antfubot Jul 16, 2026
365629d
feat(prototype): clean struct rendering + query-this-key actions
antfubot Jul 16, 2026
1663a9a
feat: refactor ui
antfu Jul 16, 2026
cbedf44
chore: update
antfu Jul 16, 2026
7af5d63
feat(prototype): keep autocomplete open across runs, solid popover, u…
antfubot Jul 16, 2026
1832a08
feat: data-inspector plugin, cross-plugin services, storage scopes
antfubot Jul 16, 2026
83fa96c
ci: register data-inspector build in the turbo graph
antfubot Jul 17, 2026
c3f7c4e
fix(data-inspector): deterministic dts emission (per-entry graphs)
antfubot Jul 17, 2026
46e5bd8
feat(data-inspector): example source, docs page, in-app docs links
antfubot Jul 17, 2026
b4d0aba
feat(data-inspector): example source inspects the environment, opt-ou…
antfubot Jul 17, 2026
b49fe61
feat: cross-plugin services and three-class storage scopes
antfubot Jul 17, 2026
1a5e4c1
chore: merge core-services-storage base branch (stacked on #107)
antfubot Jul 17, 2026
cad3bcf
Merge remote-tracking branch 'origin/main' into free-points-return
antfubot Jul 17, 2026
0147cc5
feat(data-inspector): full dev server, /inject rename, richer source …
antfubot Jul 17, 2026
4ea44d6
refactor(data-inspector): ph icons via unocss instead of inline svg
antfubot Jul 17, 2026
e782e1d
chore: update
antfu Jul 17, 2026
eb3f089
refactor(data-inspector): break App.vue into panel components
antfubot Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,18 @@ export const alias = {
'@devframes/plugin-terminals/vite': p('terminals/src/vite.ts'),
'@devframes/plugin-terminals': p('terminals/src/index.ts'),
'@devframes/plugin-git': p('git/src/index.ts'),
'devframe/recipes/interactive-auth': r('devframe/src/recipes/interactive-auth.ts'),
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
'devframe/client': r('devframe/src/client/index.ts'),
'devframe': r('devframe/src'),
'@devframes/plugin-data-inspector/client': p('data-inspector/src/client/index.ts'),
'@devframes/plugin-data-inspector/node': p('data-inspector/src/node/index.ts'),
'@devframes/plugin-data-inspector/registry': p('data-inspector/src/registry/index.ts'),
'@devframes/plugin-data-inspector/engine': p('data-inspector/src/engine/index.ts'),
'@devframes/plugin-data-inspector/inject': p('data-inspector/src/inject/index.ts'),
'@devframes/plugin-data-inspector/cli': p('data-inspector/src/cli.ts'),
'@devframes/plugin-data-inspector/vite': p('data-inspector/src/vite.ts'),
'@devframes/plugin-data-inspector': p('data-inspector/src/index.ts'),
'@devframes/plugin-inspect/client': p('inspect/src/client/index.ts'),
'@devframes/plugin-inspect/node': p('inspect/src/node/index.ts'),
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function helpersItems(prefix: string) {
function pluginsItems(prefix: string) {
return [
{ text: 'Overview', link: `${prefix}/plugins/` },
{ text: 'Data Inspector', link: `${prefix}/plugins/data-inspector` },
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
{ text: 'Git', link: `${prefix}/plugins/git` },
Expand Down
138 changes: 138 additions & 0 deletions docs/plugins/data-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
outline: deep
---

# Data Inspector

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.

Package: `@devframes/plugin-data-inspector` · framework: **Vue + Vite**

## What it does

- **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.
- **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.
- **Data shape panel** — a one-level type skeleton of the active source, independent of the query; click a property to query it.
- **Filters** — exclude functions, `_`-prefixed, or `$`-prefixed properties from results and skeleton alike.
- **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).

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

## Providing data sources

The registry is **process-global**: register from anywhere in the process — plugin setup, host hooks, application code — before or after the inspector mounts.

```ts
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'

registerDataSource({
id: 'my-plugin:store', // namespace with your plugin id
title: 'My plugin store',
description: 'The live state store',
icon: 'i-ph:database-duotone',
data: () => store,
queries: [
{ title: 'Active sessions', query: 'sessions.mapEntries().value.[active]' },
{ title: 'Config (data only)', query: 'config', excludeFunctions: true },
],
})
```

The contract:

```ts
interface DataSourceEntry {
id: string
title: string
description?: string
icon?: string
/** A plain value, or a factory returning one (sync or async). */
data: unknown | (() => unknown | Promise<unknown>)
/** The resolved value never changes: resolve once and memoize. */
static?: boolean
/** Suggested queries, shown read-only next to saved ones. */
queries?: Query[]
}
```

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.

Integrations that prefer zero package dependency consume the same registry through the typed [context service](../guide/devframe-definition#cross-plugin-services):

```ts
ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
sources.register({ id: 'my-plugin:store', title: 'My store', data: () => store })
})
```

> [!WARNING]
> 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.

## Mount into a Vite host

```ts
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
// vite.config.ts
import { dataInspectorVitePlugin } from '@devframes/plugin-data-inspector/vite'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
dataInspectorVitePlugin(),
{
name: 'my-app:data-sources',
configureServer(server) {
registerDataSource({
id: 'vite:server',
title: 'Vite Dev Server',
data: () => server,
queries: [{ title: 'Plugin names', query: 'config.plugins.name' }],
})
},
},
],
})
```

Hub hosts mount the default export like any devframe definition.

## Standalone CLI

```sh
devframe-data-inspector # the example source
devframe-data-inspector stats.json log.jsonl # one static source per data file
devframe-data-inspector build stats.json # self-contained static export
devframe-data-inspector attach # attach to a process running the agent
```

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

## Attach to another Node process

The target process opts in by starting the agent:

```ts
import { exposeDataInspector } from '@devframes/plugin-data-inspector/inject'

await exposeDataInspector()
```

or with zero code changes:

```sh
DEVFRAME_DATA_INSPECTOR=1 node --import @devframes/plugin-data-inspector/inject server.js
```

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.

## RPC surface

All functions are namespaced `devframes:plugin:data-inspector:*`:

| Function | Type | Returns |
|----------|------|---------|
| `sources` | `query` | Every registered source (meta and suggested queries). |
| `query` | `query` | Runs a jora query against a source; normalized result with stats. |
| `skeleton` | `query` | The type skeleton of a source, honoring the filter options. |
| `suggest` | `query` | Autocomplete candidates from jora's stat mode at a cursor position. |
| `saved:list` / `saved:save` / `saved:delete` | `query` / `action` | Saved-query recipes in the `workspace` and `project` scopes. |
3 changes: 2 additions & 1 deletion docs/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf

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

## One client, any framework

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

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.

Expand Down
1 change: 1 addition & 0 deletions examples/minimal-vite-devframe-hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@devframes/hub": "workspace:*",
"@devframes/plugin-a11y": "workspace:*",
"@devframes/plugin-code-server": "workspace:*",
"@devframes/plugin-data-inspector": "workspace:*",
"@devframes/plugin-git": "workspace:*",
"@devframes/plugin-inspect": "workspace:*",
"@devframes/plugin-messages": "workspace:*",
Expand Down
31 changes: 31 additions & 0 deletions examples/minimal-vite-devframe-hub/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import a11yDevframe, { a11yAgentBundlePath } from '@devframes/plugin-a11y'
import codeServerDevframe from '@devframes/plugin-code-server'
import dataInspectorDevframe from '@devframes/plugin-data-inspector'
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
import gitDevframe from '@devframes/plugin-git'
import inspectDevframe from '@devframes/plugin-inspect'
import messagesDevframe from '@devframes/plugin-messages'
Expand All @@ -18,6 +20,34 @@ export default defineConfig({
server: { allowedHosts: true, strictPort: false },
plugins: [
UnoCSS(),
{
// The host registers its own live objects as data-inspector sources —
// the registry is process-global, so this works from any plugin hook.
name: 'minimal-vite-devframe-hub:data-sources',
configureServer(server) {
registerDataSource({
id: 'vite:server',
title: 'Vite Dev Server',
description: 'The live ViteDevServer instance serving this hub.',
icon: 'i-ph:lightning-duotone',
data: () => server,
queries: [
{ title: 'Plugin names', query: 'config.plugins.name' },
{
title: 'Module graph',
description: 'Client-environment modules with their importers',
query: 'environments.client.moduleGraph.idToModuleMap.mapEntries().value.({ url, type, importers: importers.fromSet().url })',
},
{
title: 'Resolved config (clean)',
query: 'config',
excludeFunctions: true,
excludeUnderscoreProps: true,
},
],
})
},
},
minimalViteDevframeHub({
devframes: [
demoDevframe,
Expand All @@ -28,6 +58,7 @@ export default defineConfig({
terminalsDevframe,
codeServerDevframe,
inspectDevframe,
dataInspectorDevframe,
a11yDevframe,
messagesDevframe,
],
Expand Down
69 changes: 69 additions & 0 deletions plugins/data-inspector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# @devframes/plugin-data-inspector

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.

## Register a data source

The registry is **process-global** — register from anywhere, before or after the plugin mounts:

```ts
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'

registerDataSource({
id: 'my-plugin:state',
title: 'My plugin state',
description: 'The live state store',
data: () => store, // value or (async) factory; `static: true` memoizes
queries: [{ title: 'Active entries', query: 'entries.mapEntries()' }],
})
```

Integrations that prefer zero package dependency consume the same store through the typed context service:

```ts
ctx.services.whenAvailable('devframes:plugin:data-inspector:sources', (sources) => {
sources.register({ id: 'my-plugin:state', title: 'My state', data: () => store })
})
```

## Mount

```ts
// hub hosts mount the default export like any devframe:
import dataInspectorDevframe from '@devframes/plugin-data-inspector'
// Vite
import { dataInspectorVitePlugin } from '@devframes/plugin-data-inspector/vite'
```

## Standalone CLI

```sh
devframe-data-inspector stats.json trace.jsonl # inspect local data files
devframe-data-inspector build stats.json # self-contained static export
devframe-data-inspector attach # attach to a process running the agent
```

Static exports embed the dataset and run the same query engine client-side, so saved recipes stay portable.

## Attach to another Node process

```ts
import { exposeDataInspector } from '@devframes/plugin-data-inspector/inject'

await exposeDataInspector()
```

or with zero code changes:

```sh
DEVFRAME_DATA_INSPECTOR=1 node --import @devframes/plugin-data-inspector/inject server.js
```

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.

> [!WARNING]
> 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.

## Saved queries

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`).
13 changes: 13 additions & 0 deletions plugins/data-inspector/bin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node
import process from 'node:process'
import { createDataInspectorCli } from './dist/cli.mjs'

async function main() {
const cli = createDataInspectorCli()
await cli.parse()
}

main().catch((error) => {
console.error(error)
process.exit(1)
})
Loading
Loading