Devframe Definition
One defineDevframe call returns a portable DevframeDefinition any adapter consumes.
Minimal definition
import { defineDevframe, defineRpcFunction } from 'devframe'
import * as v from 'valibot' // npm i valibot
export default defineDevframe({
id: 'my-tool',
name: 'My Tool',
version: '1.0.0',
packageName: 'my-tool',
importMetaUrl: import.meta.url,
homepage: 'https://github.com/me/my-tool',
description: 'A one-line summary of what the tool does.',
icon: 'ph:gauge-duotone',
setup(ctx) {
// A scoped context auto-namespaces ids with your devframe `id`.
const my = ctx.scope('my-tool')
// Register your RPC functions, shared state, etc. here.
my.rpc.register(defineRpcFunction({
name: 'hello', // stored as `my-tool:hello`
type: 'static',
jsonSerializable: true,
handler: () => ({ message: 'hello' }),
}))
},
})Definition fields
id, name, version, packageName, homepage, description, and setup are required; pass importMetaUrl: import.meta.url so remote assets and declared services resolve against the devframe's own dependencies. The remaining fields cover display (icon), mounting (basePath, duplicationStrategy, capabilities), what the devframe consumes and serves (services, clientAssets, rpc.snapshot), and CLI defaults (cli). Every field is listed in the Node-Side API reference.
Sourcing metadata from package.json
Import metadata from package.json (name → packageName; devframe name is a separate display label):
import pkg from '../package.json' with { type: 'json' }
export default defineDevframe({
id: 'my-tool',
name: 'My Tool', // display label
version: pkg.version,
packageName: pkg.name,
importMetaUrl: import.meta.url,
homepage: pkg.homepage,
description: pkg.description,
setup(ctx) { /* … */ },
})Resolving against the devframe's own dependencies
importMetaUrl resolves companion packages against the devframe's own dependencies:
export default defineDevframe({
/** …metadata as above */
importMetaUrl: import.meta.url,
/**
* Served from the locally installed `my-tool--assets`, resolved via
* `importMetaUrl`; it works under pnpm's strict layout with zero network.
*/
clientAssets: { package: `${pkg.name}--assets`, version: pkg.version },
/** Imported from `my-tool`'s own dependency graph. */
services: [{ package: '@scope/my-service', version: pkg.version }],
setup(ctx) { /* … */ },
})For remote assets, importMetaUrl is the default resolveFrom; a per-source value wins; resolveFrom: null opts out.
Serving the UI with clientAssets
import { fileURLToPath } from 'node:url'
export default defineDevframe({
/** …metadata as above */
importMetaUrl: import.meta.url,
/**
* A local build resolved from the module; it works from source and the
* published package.
*/
clientAssets: fileURLToPath(new URL('../dist/spa', import.meta.url)),
setup(ctx) { /* … */ },
})For assets you host yourself, call ctx.views.hostStatic in setup; see Client Assets.
Runtime flags
ctx.mode ('dev'/'build') gates runtime-specific work:
defineDevframe({
id: 'my-tool',
name: 'My Tool',
setup(ctx) {
if (ctx.mode === 'build') {
// Static-only work, baked into the RPC dump.
ctx.rpc.addFunctions(staticFunctions)
}
else {
// Dev-mode wiring, file watchers, etc.
watchProject(ctx)
}
},
})The CLI dev server sets mode: 'dev'; createBuild, 'build'.
The setup context
setup(ctx) receives a DevframeNodeContext:
interface DevframeNodeContext {
readonly cwd: string
readonly workspaceRoot: string
readonly mode: 'dev' | 'build'
host: DevframeHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir)
rpc: RpcFunctionsHost // register + broadcast + sharedState
views: DevframeViewHost // static file hosting (`hostStatic`)
diagnostics: DevframeDiagnosticsHost
agent: DevframeAgentHost // expose tools + resources to coding agents
services: DevframeServicesHost // typed cross-devframe service registry
staticConfig: Partial<DevframeConnectionConfigsRegistry> // this context's own ConnectionMeta.configs
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
}Cross-devframe services
ctx.services is a typed, namespaced registry: one devframe exposes a capability, others consume it (Cross-Devframe Services).
ctx.services.provide('my-plugin:sources', sources)
ctx.services.whenAvailable('my-plugin:sources', (sources) => {
sources.register(/* ... */)
})Static connection configs
ctx.staticConfig is this context's own ConnectionMeta.configs: read-only boot-time data from the connection handshake; write it during setup(ctx). Contrast ctx.scope(id).settings (mutable, synced).
declare module 'devframe/types' {
interface DevframeConnectionConfigsRegistry {
'my-plugin': { featureFlag: boolean }
}
}
ctx.staticConfig['my-plugin'] = { featureFlag: true }Storage scopes
ctx.host.getStorageDir(scope) places persisted state in three classes: workspace is committable and team-shared (saved presets, config), project is per-checkout (caches, personal settings), and global is per-user (auth tokens, machine-wide prefs). Placements are in the Node-Side API reference.
ctx.scope(id) returns a namespace-scoped view (Scoped Context) auto-prefixing every RPC id, shared-state key, and streaming channel, plus a persisted settings store (project/global scopes use the matching storage classes).
Hosted adapters can augment ctx, e.g. the vite adapter's dock, command, message, and terminal hosts.
CLI options
cli sets CLI-adapter defaults and plugs flags/commands into CAC:
defineDevframe({
id: 'my-tool',
name: 'My Tool',
clientAssets: './client/dist', // built SPA served as the UI
cli: {
command: 'my-tool', // binary name; default: the `id`
port: 9876, // preferred port; default: 9999
portRange: [9876, 10000], // forwarded to get-port-please
random: false, // forwarded to get-port-please
host: 'localhost', // default host; --host overrides
open: true, // auto-open the browser on dev start; embeds the current OTP so the tab lands authenticated
configure(cli) { // contribute capability flags/commands
cli
.option('--my-flag <value>', 'Tool-specific flag')
},
},
setup(ctx, { flags }) {
// `flags` carries the parsed cac bag: the built-in flags
// (`--port`, `--host`, `--open`, `--no-open`) and anything you added
// in `configure`.
},
})Beyond the fields shown, random prefers a random open port and auth: false disables the WS trust flow for localhost-only, single-user tools. Every field is listed in the Node-Side API reference.
Multiple runtimes, one definition
Wire the definition into multiple adapters from one file:
import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
import { createBuild } from 'devframe/adapters/build'
import { createCac } from 'devframe/adapters/cac'
const myDevframe = defineDevframe({ id: 'my-tool', name: 'My Tool', setup() {} })
// 1. Standalone CLI:
await createCac(myDevframe).parse()
// 2. Offline snapshot:
await createBuild(myDevframe, { outDir: 'dist-static' })
/** 3. Mount into a host framework (Vite DevTools shown; others can implement equivalents): */
export const myPlugin = () => createPluginFromDevframe(myDevframe)What's next
- Adapters: deployment targets
- RPC: register node-side functions
viteadapter: mount into a host framework
Tutorial: Build a Server Data Inspector
Build a devtool that displays and queries live server-side data, then ship it as a hub dock entry, a static build, a standalone dev server, and a CLI.
RPC
Type-safe, bidirectional RPC built on birpc, validated against any Standard Schema validator. Dev runs over WebSocket; build/SPA serves a pre-computed static dump.