You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -3,48 +3,28 @@ title: 'Tutorial: Build a Data Inspector'
3
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.'
4
4
---
5
5
6
-
In this walkthrough you build a real devtool from an empty folder: a **Data Inspector** that shows the shape of your server's live state and lets you read any value out of it. You'll get it working end to end first, then add one capability at a time — a dock inside a hub, a static build, a standalone server, and a CLI.
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
7
8
-
Each step introduces exactly one new idea. Nothing here assumes you've read the rest of the guide; links point to the deeper reference when you want it.
8
+
One new idea per step, nothing assumed. You'll need **Node 24+** (so `node` runs TypeScript directly) and a terminal.
9
9
10
-
**You'll need** Node 24+ (so `node` runs the TypeScript files directly) and a terminal. Every code block is complete — copy them as you go.
10
+
## The shape of a devframe app
11
11
12
-
## What we're building
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.
13
13
14
-
A devframe app has two halves that talk over a typed connection:
14
+
## Step 1 — Define the tool
15
15
16
-
- a **server** half that runs in your Node process and exposes functions (here: "what does the state look like?" and "give me the value at this path"),
17
-
- a **browser** half — a small web UI that calls those functions and shows the answers.
18
-
19
-
Devframe's job is the wire between them, plus everything around it: serving the UI, the live connection, authentication, static builds, a CLI. You write the two halves once; devframe runs them everywhere.
20
-
21
-
Let's start.
22
-
23
-
## Step 0 — An empty project
24
-
25
-
Make a folder and initialize it:
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:
26
17
27
18
```sh
28
19
mkdir data-inspector &&cd data-inspector
29
-
npm init -y
30
-
npm pkg set type=module
31
-
npm install devframe
32
-
npm install -D typescript
20
+
npm init -y && npm pkg set type=module
21
+
npm install devframe && npm install -D typescript
33
22
```
34
23
35
-
That's the only dependency for the first milestone. We'll add more as each capability calls for it.
36
-
37
-
## Step 1 — Define the tool and its first function
38
-
39
-
Everything begins with one call: `defineDevframe`. It pairs your tool's identity with a `setup` function where you register what it can do.
40
-
41
-
Create `src/devframe.ts`:
42
-
43
24
```ts [src/devframe.ts]
44
25
import { defineDevframe } from'devframe'
45
26
46
-
// The live server state our tool inspects. In a real app this might be your
47
-
// config, a cache, a database handle — anything living in the process.
27
+
// Whatever you want to peek at while your app runs — config, a cache, a DB handle.
-**The definition** — `id`, `name`, and a bit of metadata identify the tool; `setup(ctx)` is where you wire up its capabilities. (Every field is covered in [Devframe Definition](/guide/devframe-definition).)
105
-
-**RPC functions** — `ctx.rpc.register` publishes a function the browser can call. Each has a namespaced `name`, a `type` (`query` means read-only), and a `handler`. The handler receives the call's arguments and returns a value; `jsonSerializable: true` promises the result is plain JSON. ([RPC](/guide/rpc) covers the other types.)
106
-
107
-
There's no UI yet, and nothing runs. Both come next.
82
+
`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.)
108
83
109
-
## Step 2 — A browser UI that calls the functions
84
+
## Step 2 — Add a UI
110
85
111
-
Now the other half. We'll use React with Vite — pick any framework you like; the only devframe-specific part is `connectDevframe`, which opens the typed connection back to the server.
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.
112
87
113
88
```sh
114
89
npm install react react-dom @devframes/vite
115
90
npm install -D vite @vitejs/plugin-react @types/react @types/react-dom
116
91
```
117
92
118
-
Create the UI in a `client/` folder:
119
-
120
93
```html [client/index.html]
121
94
<!doctype html>
122
95
<html>
@@ -152,9 +125,8 @@ export function App() {
152
125
const [result, setResult] =useState<unknown>()
153
126
154
127
useEffect(() => {
155
-
// Connect once. With no argument, the client discovers where the server
156
-
// lives from the page's own URL — so this works unchanged no matter how
157
-
// the tool ends up hosted (dev server, hub, standalone).
128
+
// No argument: the client finds the server from the page's own URL, so
129
+
// this line never changes no matter how the tool is hosted.
`connectDevframe()` returns a client whose `.call(name, ...args)` reaches your server functions. (We cast `.call` to call by name for brevity; once you register functions through a typed registry, every call is checked end to end — see [RPC](/guide/rpc).)
194
-
195
-
Still nothing to run — the two halves aren't connected yet.
196
-
197
-
## Step 3 — Run it (the MVP)
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).)
198
166
199
-
The server half needs to be served *somewhere*. The quickest way while developing is to let Vite's dev server host the UI and hand the RPC traffic to devframe. `@devframes/vite` provides exactly that bridge.
167
+
## Step 3 — Run it
200
168
201
-
Create `vite.config.ts`:
169
+
The two halves still need to meet. While developing, let Vite serve the UI and hand RPC traffic to devframe:
Open the printed URL. You should see the three top-level keys with their types, and typing a path like `config.port` or `users.0.name` and pressing**Query** prints the value. That round trip — button → `rpc.call` → your `handler` → back to the page — is a devframe app working.
194
+
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.
231
195
232
196
> [!WARNING]
233
-
> `auth: false` trusts any connection that can reach the port. It's fine for a localhost demo, but leave it off (devframe gates with a one-time code by default) for anything reachable beyond your machine. See [Security](/guide/security).
234
-
235
-
That's the **MVP**: one definition, two functions, a UI, live over a dev server. Everything from here reuses this exact `src/devframe.ts` and `client/` — we only change how they're *hosted*.
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).
236
198
237
-
## Step 4 — Show it as a dock in a hub
199
+
Everything below reuses this exact `src/devframe.ts` and `client/`. We only change where they run.
238
200
239
-
A [hub](/guide/hub) gathers many devframes behind one interface, each appearing as a **dock** you switch between — the tool's own UI shown in an iframe. Because our client already connects with a bare `connectDevframe()`, it works wherever it's mounted — so this takes just one change to the definition.
201
+
## Step 4 — Dock it in a hub
240
202
241
-
The hub serves your UI itself (rather than Vite serving it), so it needs the built assets. Tell the definition where they'll be, by adding one field to `defineDevframe` in `src/devframe.ts`:
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:
242
204
243
205
```ts [src/devframe.ts]
244
206
import { fileURLToPath } from'node:url'
245
207
// …
246
-
247
208
exportdefaultdefineDevframe({
248
209
id: 'data-inspector',
249
210
// …
250
-
importMetaUrl: import.meta.url,
251
-
// The built UI devframe serves when it hosts the SPA itself.
Open the printed URL: your Data Inspector now appears as a dock in the hub's rail, its UI running in an iframe. Add more devframes to that `devframes: [...]`array — your own or the [built-in plugins](/plugins) — and each becomes another dock. The hub prints a one-time code on startup; enter it to authorize.
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.)
288
245
289
-
## Step 5 — Ship a static build
246
+
## Step 5 — Build a static version
290
247
291
-
Some tools should be viewable with no server at all — a report you can drop on any static host. `createBuild` renders your UI to a folder and **bakes** the results of read-only calls into it, so the built page answers them without a live process.
292
-
293
-
Opt a function into the bake by adding `snapshot: true`. In `src/devframe.ts`, mark `get-meta`:
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`:
294
249
295
250
```ts
296
251
ctx.rpc.register({
297
252
name: 'data-inspector:get-meta',
298
253
type: 'query',
299
254
jsonSerializable: true,
300
-
snapshot: true, // bake this call's result into the static build
255
+
snapshot: true, // bake this call's result into the build
npx vite build # refresh dist/client (the UI createBuild copies in)
316
-
node scripts/build.mjs # → dist-static/, a self-contained deploy
268
+
npx vite build # refresh dist/client
269
+
node scripts/build.mjs # → dist-static/
317
270
```
318
271
319
-
Serve `dist-static/`with any static file server and the meta list renders from the baked snapshot — no Node process running. `query` takes an argument, so it isn't baked by default; a query needs its live server (next step), or you can bake specific inputs (see [Client Assets](/guide/client-assets)). This is the same result the CLI's `build` command produces in Step 7.
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)).
320
273
321
-
## Step 6 — Run it standalone (no Vite)
274
+
## Step 6 — Run it standalone
322
275
323
-
Vite was convenient for development, but the definition doesn't depend on it. `createDevServer` runs your tool as its own server, serving the UI from `clientAssets` and answering RPC live — no host framework involved.
276
+
The definition never depended on Vite. `createDevServer` runs the tool on its own, serving the UI from `clientAssets` and answering RPC live:
The same UI and the same live `query`/`get-meta` — now hosted entirely by devframe. This is what you'd embed in your own Node program when you want the tool available without a bundler in the loop.
290
+
Same UI, same live calls — no bundler in the loop. This is what you'd drop into your own Node program.
338
291
339
292
## Step 7 — Give it a CLI
340
293
341
-
Finally, wrap the standalone server in a command shell so anyone can run the tool without writing a script. `createCac`turns your definition into a CLI with `dev`, `build`, and `mcp`subcommands out of the box.
294
+
Finally, wrap that server in a command shell. `createCac`hands you `dev`, `build`, and `mcp`for free:
342
295
343
296
```js [bin.mjs]
344
297
#!/usr/bin/env node
@@ -348,38 +301,19 @@ import devframe from './src/devframe.ts'
348
301
createCac(devframe).parse()
349
302
```
350
303
351
-
Wire it up as the package's binary:
352
-
353
304
```sh
354
305
npm pkg set bin.data-inspector=bin.mjs
355
-
```
356
-
357
-
Now the three modes from the previous steps are subcommands:
358
306
359
-
```sh
360
307
node bin.mjs dev # the standalone server from Step 6
361
308
node bin.mjs build # the static build from Step 5
362
309
node bin.mjs mcp # expose the tool to a coding agent over MCP
363
310
```
364
311
365
-
Published to npm, that same binary runs with `npx data-inspector`. One definition, five ways to run it — and you never rewrote the tool to get there.
366
-
367
-
## Where you've been
368
-
369
-
You wrote two halves once and grew the hosting around them:
370
-
371
-
| Step | Capability | Key API |
372
-
|------|------------|---------|
373
-
| 1–3 | Live UI + RPC over a dev server |`defineDevframe`, `ctx.rpc.register`, `connectDevframe`, `devframeViteBridge`|
374
-
| 4 | A dock inside a hub |`clientAssets`, `viteDevframeHub`, `createUi`|
0 commit comments