Devframe Definition

One defineDevframe call returns a portable DevframeDefinition any adapter consumes.

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 (namepackageName; 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