Skip to content

Commit a8cbd16

Browse files
committed
chore: update
1 parent c105e7c commit a8cbd16

1 file changed

Lines changed: 59 additions & 52 deletions

File tree

docs/content/1.guide/1.tutorial.md renamed to docs/content/1.guide/1.tutorial-server-data-inspector.md

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
---
2-
title: 'Tutorial: Build a Data Inspector'
3-
description: 'Build a small devtool from an empty folder — a live view into your server''s state — then grow it one capability at a time: a dock in a hub, a static build, a standalone server, and a CLI.'
2+
title: 'Tutorial: Build a Server Data Inspector'
3+
description: 'Build a Devframe to display and query data from the server side, with a dock in a hub, a static build, a standalone server, and a CLI.'
44
---
55

6-
Let's build a real devtool from nothing: a **Data Inspector** that shows the shape of your server's live state and lets you read any value out of it. We'll get it working first, then teach it new tricks one at a time — a dock in a hub, a static build, a standalone server, a CLI.
7-
8-
One new idea per step, nothing assumed. You'll need **Node 24+** (so `node` runs TypeScript directly) and a terminal.
6+
Let's build a real devtool from sketch: a **Data Inspector** that shows the shape of your server's live state and lets you read any value out of it. We'll get it working first, then teach it new tricks one at a time, a dock in a hub, a static build, a standalone server, a CLI.
97

108
## The shape of a devframe app
119

12-
Two halves talk over a typed connection: a **server** half in your Node process that exposes functions, and a **browser** half that calls them and shows the answers. Devframe is everything in between — the wire, the UI hosting, auth, builds, a CLI. Write the two halves once; run them anywhere.
10+
Two halves talk over a typed connection: a **server** in your Node process that exposes server functions, and a **browser** client that gets the data and renders them nicely and provides interactivity. Devframe is everything in between, the wire, the UI hosting, auth, builds, a CLI.
11+
12+
## Step 1, Define the tool
13+
14+
Everything starts with `defineDevframe`: your tool's name, plus a `setup` where you register what it can do.
1315

14-
## Step 1 — Define the tool
16+
> You will need [Node 24+](https://nodejs.org/) for this tutorial.
1517
16-
Everything starts with `defineDevframe`: your tool's name, plus a `setup` where you register what it can do. Create the project and the definition:
18+
Create the project and the definition:
1719

1820
```sh
1921
mkdir data-inspector && cd data-inspector
2022
npm init -y && npm pkg set type=module
2123
npm install devframe && npm install -D typescript
2224
```
2325

24-
```ts [src/devframe.ts]
26+
```ts [src/data-inspector.ts]
2527
import { defineDevframe } from 'devframe'
2628

27-
// Whatever you want to peek at while your app runs — config, a cache, a DB handle.
29+
// An example server-side data, whatever you want to peek at while your app runs, config, a cache, a DB handle.
2830
const serverState = {
2931
config: { name: 'Acme', port: 3000, debug: false },
3032
users: [
@@ -34,7 +36,7 @@ const serverState = {
3436
featureFlags: { newDashboard: true, betaSearch: false },
3537
}
3638

37-
// Follow a dot-path like `users.0.name` into the state.
39+
// A simple query function that follows a dot-path like `users.0.name` into the state.
3840
function valueAtPath(root: unknown, path: string): unknown {
3941
if (!path)
4042
return root
@@ -45,7 +47,7 @@ function valueAtPath(root: unknown, path: string): unknown {
4547
}, root)
4648
}
4749

48-
export default defineDevframe({
50+
const dataInspectorFrame = defineDevframe({
4951
id: 'data-inspector',
5052
name: 'Data Inspector',
5153
version: '0.0.0',
@@ -77,13 +79,15 @@ export default defineDevframe({
7779
})
7880
},
7981
})
82+
83+
export default dataInspectorFrame
8084
```
8185

8286
`ctx.rpc.register` publishes a function the browser can call: a namespaced `name`, a `type` (`query` is read-only), and a `handler` that takes the call's arguments and returns JSON. That's the whole server. ([RPC](/guide/rpc) has the other types; [Devframe Definition](/guide/devframe-definition) has every field.)
8387

84-
## Step 2 — Add a UI
88+
## Step 2, Add a UI
8589

86-
Now the browser half. We'll use React, but any framework works — the only devframe-specific line is `connectDevframe`, which opens the connection home.
90+
Now the browser part. We'll use React for this example, but any framework works, the only devframe-specific line is `connectDevframe`, which opens the connection home.
8791

8892
```sh
8993
npm install react react-dom @devframes/vite
@@ -148,7 +152,7 @@ export function App() {
148152
{meta.map(m => (
149153
<li key={m.key}>
150154
<code>{m.key}</code>
151-
{' — '}
155+
{' - '}
152156
{m.type}
153157
{m.length != null ? ` (${m.length})` : ''}
154158
</li>
@@ -162,17 +166,17 @@ export function App() {
162166
}
163167
```
164168

165-
`client.call(name, ...args)` reaches your handlers. (We cast `.call` to call by name; wire up a typed registry later and every call is checked end to end — see [RPC](/guide/rpc).)
169+
`client.call(name, ...args)` reaches your handlers. (We cast `.call` to call by name; wire up a typed registry later and every call is checked end to end, see [RPC](/guide/rpc).)
166170

167-
## Step 3 — Run it
171+
## Step 3, Run it as Development
168172

169-
The two halves still need to meet. While developing, let Vite serve the UI and hand RPC traffic to devframe:
173+
To try the tool we just built, we can let Vite serve the UI and hand RPC traffic to devframe:
170174

171-
```ts [vite.config.ts]
175+
```ts [vite.client.config.ts]
172176
import { devframeViteBridge } from '@devframes/vite/single'
173177
import react from '@vitejs/plugin-react'
174178
import { defineConfig } from 'vite'
175-
import devframe from './src/devframe.ts'
179+
import dataInspectorFrame from './src/data-inspector.ts'
176180

177181
export default defineConfig({
178182
root: 'client',
@@ -181,26 +185,26 @@ export default defineConfig({
181185
plugins: [
182186
react(),
183187
// Vite serves the page; the bridge answers RPC on the same origin, so
184-
// `connectDevframe()` just finds it. `auth: false` — see the note below.
185-
devframeViteBridge(devframe, { base: '/', auth: false }),
188+
// `connectDevframe()` just finds it. `auth: false`, see the note below.
189+
devframeViteBridge(dataInspectorFrame, { base: '/', auth: false }),
186190
],
187191
})
188192
```
189193

190194
```sh
191-
npx vite
195+
npx vite --config vite.client.config.ts
192196
```
193197

194198
Open the printed URL. Three keys with their types show up, and typing `config.port` or `users.0.name` and hitting **Query** prints the value. Button → `call` → your `handler` → back to the page: that's the whole app, working.
195199

196200
> [!WARNING]
197-
> `auth: false` trusts anything that can reach the port — fine for localhost, but leave it out (devframe gates with a one-time code by default) for anything reachable from elsewhere. See [Security](/guide/security).
201+
> `auth: false` trusts anything that can reach the port. We have it off to make the tutorial easier. But we strongly recommend enabling it if you are publishing it as a tool. See [Security](/guide/security).
198202
199203
Everything below reuses this exact `src/devframe.ts` and `client/`. We only change where they run.
200204

201-
## Step 4 — Dock it in a hub
205+
## Step 4, Dock it in a hub
202206

203-
A [hub](/guide/hub) puts many devframes behind one interface, each a **dock** you switch between — the tool's own UI in an iframe. Since our client uses a bare `connectDevframe()`, it already works anywhere; the hub just needs the built UI. Point the definition at it:
207+
A [hub](/guide/hub) puts many devframes behind one interface, each a **dock** you switch between, the tool's own UI in an iframe. Since our client uses a bare `connectDevframe()`, it already works anywhere; the hub just needs the built UI. Point the definition at it:
204208

205209
```ts [src/devframe.ts]
206210
import { fileURLToPath } from 'node:url'
@@ -217,88 +221,89 @@ Build the UI and stand up a one-devframe hub:
217221

218222
```sh
219223
npm install @devframes/hub @devframes/hub-ui
220-
npx vite build
224+
npx vite build --config vite.client.config.ts
221225
```
222226

223-
```ts [hub.config.ts]
227+
```ts [vite.hub.config.ts]
224228
import { createUi } from '@devframes/hub-ui'
225229
import { viteDevframeHub } from '@devframes/vite/hub'
226230
import { defineConfig } from 'vite'
227-
import devframe from './src/devframe.ts'
231+
import dataInspectorFrame from './src/data-inspector.ts'
228232

229233
export default defineConfig({
230234
plugins: [
231235
viteDevframeHub({
232-
devframes: [devframe],
236+
devframes: [dataInspectorFrame],
233237
ui: createUi({ branding: { productName: 'My Devtools' } }),
234-
quiet: true,
235238
}),
236239
],
237240
})
238241
```
239242

240243
```sh
241-
npx vite --config hub.config.ts
244+
npx vite --config vite.hub.config.ts
242245
```
243246

244-
Your inspector now sits in the hub's rail as a dock. Drop more into `devframes: [...]` — your own or the [built-in plugins](/plugins) — and each gets its own. (The hub prints a code to authorize on first connect.)
247+
Your inspector now sits in the hub's rail as a dock. Drop more into `devframes: [...]`, your own or the [built-in plugins](/plugins), and each gets its own. (The hub prints a code to authorize on first connect.)
245248

246-
## Step 5 — Build a static version
249+
## Step 5, Build a static version
247250

248-
Some tools should work with no server at all — a report you can drop on any static host. `createBuild` renders the UI and **bakes in** the results of read-only calls. Opt one in with `snapshot: true`:
251+
Some tools should work with no server at all, a report you can drop on any static host. `createBuild` renders the UI and **bakes in** the results of read-only calls. Opt one in with `snapshot: true`:
249252

250253
```ts
251254
ctx.rpc.register({
252255
name: 'data-inspector:get-meta',
253256
type: 'query',
254257
jsonSerializable: true,
255258
snapshot: true, // bake this call's result into the build
256-
handler: () => /* … unchanged … */,
259+
handler: () => {
260+
/* … unchanged … */
261+
}
257262
})
258263
```
259264

260265
```js [scripts/build.mjs]
261266
import { createBuild } from 'devframe/adapters/build'
262-
import devframe from '../src/devframe.ts'
267+
import dataInspectorFrame from '../src/data-inspector.ts'
263268

264-
await createBuild(devframe, { outDir: 'dist-static' })
269+
await createBuild(dataInspectorFrame, { outDir: 'dist-static' })
265270
```
266271

267272
```sh
268273
npx vite build # refresh dist/client
269274
node scripts/build.mjs # → dist-static/
270275
```
271276

272-
Serve `dist-static/` anywhere and the meta list renders from the baked snapshot, no Node in sight. `query` takes an argument, so it needs the live server (next) — or bake specific inputs ([Client Assets](/guide/client-assets)).
277+
Serve `dist-static/` anywhere and the meta list renders from the baked snapshot, no Node in sight. `query` takes an argument, so it needs the live server (next), or bake specific inputs ([Client Assets](/guide/client-assets)).
273278

274-
## Step 6 — Run it standalone
279+
## Step 6, Run it standalone
275280

276281
The definition never depended on Vite. `createDevServer` runs the tool on its own, serving the UI from `clientAssets` and answering RPC live:
277282

278283
```js [scripts/serve.mjs]
279284
import { createDevServer } from 'devframe/adapters/dev'
280-
import devframe from '../src/devframe.ts'
285+
import dataInspectorFrame from '../src/data-inspector.ts'
281286

282-
await createDevServer(devframe, { openBrowser: true })
287+
await createDevServer(dataInspectorFrame, { openBrowser: true })
283288
```
284289

285290
```sh
286291
npx vite build
287292
node scripts/serve.mjs
288293
```
289294

290-
Same UI, same live calls — no bundler in the loop. This is what you'd drop into your own Node program.
295+
Same UI, same live calls, no bundler in the loop. This is what you'd drop into your own Node program.
291296

292-
## Step 7 — Give it a CLI
297+
## Step 7, Give it a CLI
293298

294-
Finally, wrap that server in a command shell. `createCac` hands you `dev`, `build`, and `mcp` for free:
299+
Finally, if you want to provide a CLI for standalone use, which wraps that server in a command shell. `devframe/adapters/cac` made it easy to automatically turn a devframe into a CLI with `dev`, `build`, and `mcp` commands. For example:
295300

296301
```js [bin.mjs]
297302
#!/usr/bin/env node
298303
import { createCac } from 'devframe/adapters/cac'
299-
import devframe from './src/devframe.ts'
304+
import dataInspectorFrame from './src/data-inspector.ts'
300305

301-
createCac(devframe).parse()
306+
createCac(dataInspectorFrame).parse()
302307
```
303308

304309
```sh
@@ -309,11 +314,13 @@ node bin.mjs build # the static build from Step 5
309314
node bin.mjs mcp # expose the tool to a coding agent over MCP
310315
```
311316

312-
Publish it and the same binary runs with `npx data-inspector`. One definition, five ways to run it — and you never rewrote the tool.
317+
While you can also build a CLI on your own with the functions provides above.
318+
319+
Let's all for this tutorial. If you want to see a full-featured server data inspector, we have it a as a ready-to-use [plugin](/plugins/data-inspector) that you can play with or reference to.
313320

314321
## What's next
315322

316-
- [RPC](/guide/rpc) — `action` and `event` calls, end-to-end types, schema validation
317-
- [Shared State](/guide/shared-state) — push live changes to the UI without polling
318-
- [Hub](/guide/hub) — docks, commands, terminals across many tools
319-
- [Agent-Native](/guide/agent-native) — expose your tool to coding agents over MCP
323+
- [RPC](/guide/rpc), `action` and `event` calls, end-to-end types, schema validation
324+
- [Shared State](/guide/shared-state), push live changes to the UI without polling
325+
- [Hub](/guide/hub), docks, commands, terminals across many tools
326+
- [Agent-Native](/guide/agent-native), expose your tool to coding agents over MCP

0 commit comments

Comments
 (0)