Devframeでクロスフレームワーク対応の開発者ツールを構築する
Devframeでクロスフレームワーク開発ツールを構築。ツールを一度定義し、標準ハンドラーとWebSocketをVite、Hono、Next.jsなどに組み込みます。
Devframeは、MITライセンスで提供されるフレームワーク非依存の開発者ツール基盤です。defineDevframe()でツールを一度定義すれば、initDevframe()がその定義をWeb Standardのリクエストハンドラーに変換し、互換性のある任意のホストにマウントできます。
ライブラリをメンテナンスしていれば、React Developer Toolsのような、自分のライブラリの内部構造を確認できるインスペクターパネルが欲しいと思ったことがあるでしょう。しかし、そこで対応マトリクスの問題に直面します。Viteプラグイン、Next.jsのルート、Honoのミドルウェアと、それぞれに専用のサーバー連携コードが必要になるのです。
本記事では、小さなDevframeツールを作成してハンドラーに変換し、同じ定義をViteとHonoにマウントします。続いて、その他のホストとパッケージ化されたアダプターを紹介し、最後にブラウザー拡張機能のほうがシンプルなケースについて解説します。
重要なポイント
devframeのdefineDevframe()でツールを一度だけ定義します。devframe/initiateのinitDevframe(def, { base })がそれを実行可能なインスタンスに変換し、handlerとnodeMiddlewareを公開します。- Hono、Nitro、Next.js、SvelteKitは
devtools.handlerをマウントします。これはWeb StandardのRequestを受け取り、Responseを返します。ViteとRsbuildはdevtools.nodeMiddlewareをマウントします。 - 実行中のdevframeは独自のポートを開きません。RPC用WebSocketをどこに置くかはホスト側が決めます。選択肢は共有サーバー、
attach()、ws: { sidecar: true }です。 - Devframeはデフォルトでアクセスを制限します。
auth: falseが適切なのは、localhost上の単一ユーザー環境に限られます。 - Next.jsとSvelteKitでは、
/__my-tool/を%5F_my-toolという名前のルートフォルダーから配信し、どちらもサイドカーWebSocketが必要です。
Devframeはどのような課題を解決するのか
Devframeが解決するのは「フレームワークごとに1つのプラグイン」という問題です。共通レイヤーがなければ、あらゆる開発者ツールがホストフレームワークごとに個別の統合を提供しなければなりません。Devframeは、ツールと実行環境の間に単一の標準ハンドラーを置くことで、この重複を解消します。v1.0のアナウンスでは、このハンドラーをツールの境界として位置づけています。UI、接続情報、ライブRPC、認証、そしてオプションのMCPエンドポイントは、すべて単一のパスプレフィックス配下でこのハンドラーの背後に配置されます。
ハンドラーが扱うのはRequestとResponseだけなので、ツールが特定の開発サーバーのAPIに依存することはありません。v1.0リリースで公開インターフェースが安定化しており、ソースコードはMITライセンスのもとdevframes/devframeリポジトリで公開されています。
Devframeツールの定義方法
Devframeの定義は、defineDevframe()が返すプレーンなオブジェクトです。ツール名を指定し、setup(ctx)内でRPC関数を登録します。今回のサンプルツールは環境変数の名前のみを一覧表示し、値は一切扱いません。
インストール手順はDevframeのドキュメントを参照してください。まずは定義から始めます。
// my-tool.ts
import { defineDevframe } from 'devframe'
import { listEnvNames } from './rpc'
export default defineDevframe({
id: 'my-tool',
name: 'Env Names',
// Required metadata and the browser entry are omitted here.
// See the Devframe docs for the full definition.
setup(ctx) {
ctx.scope('my-tool').rpc.register(listEnvNames)
},
})
rpc.tsでは、Object.keys(process.env).sort()を返すハンドラーを持つRPC関数としてlistEnvNamesをエクスポートします。ラッパーの形式とオプションについてはDevframe定義ガイドで解説されています。このガイドには完全な定義に必要なフィールドも記載されており、id、name、version、packageName、homepage、description、setupが必須で、さらにimportMetaUrl: import.meta.urlも渡す必要があります。また、スコープ付きコンテキストではRPCのIDがdevframeのidで名前空間化されることも説明されています。
名前だけを返すのは、データ露出を意識した意図的な設計です。認証ゲートはパネルを保護しますが、そもそも値を読み取らない関数であれば、誰がパネルを開いたとしても値が漏洩することはありません。
定義をハンドラーに変換する
initDevframe()は定義をライブインスタンスに変換し、そのインスタンスのhandlerが1つのベースパス配下でツール全体を配信します。定義とbaseを渡します。
// devtools.ts
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'
export const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
Standard Handlerのドキュメントによれば、1つのマウントベースでツール全体をカバーします。ハンドラーはSPA、__connection.jsonディスカバリーファイル、RPCソケット、認証ゲート、MCPルートを配信します。baseオプションは必須です。handlerとnodeMiddlewareはどちらも内部で準備完了を待機するため、すぐにマウントして問題ありません。インスタンスは、ソケットとライフサイクルを制御するためのattach、handleUpgrade、close()も公開しています。
Viteにマウントする
ViteはConnectスタイルのミドルウェアを使用するため、DevframeツールをViteにマウントするには、configureServerフック内でdevtools.nodeMiddlewareを使います。ViteのHTTPサーバーを共有できるよう、インスタンスはフック内で作成します。
// vite.config.ts
import { defineConfig } from 'vite'
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'
export default defineConfig({
plugins: [
{
name: 'my-tool',
apply: 'serve',
configureServer(server) {
const devtools = initDevframe(myDevframe, {
base: '/__my-tool/',
server: server.httpServer ?? undefined,
})
server.middlewares.use(devtools.nodeMiddleware)
},
},
],
})
apply: 'serve'を指定することで、パネルが本番ビルドに含まれなくなります。serverオプションによってRPC用WebSocketがVite自身のHTTPサーバー上に配置されるため、追加のポートは開かれません。Rsbuildも同様にnodeMiddlewareを受け付けますが、そのハンドラーにはアップグレードリクエストが届かないため、ソケット用にws: { sidecar: true }が必要です。
同じツールをHonoにマウントする
HonoはWeb Standardのリクエストをネイティブに扱うため、生のリクエストをdevtools.handlerに渡し、WebSocketをNodeサーバーにアタッチします。先ほどのdevtools.tsモジュールをそのまま再利用し、変更するのは連携コードだけです。
// server.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { devtools } from './devtools'
const app = new Hono()
app.all('/__my-tool/*', c => devtools.handler(c.req.raw))
devtools.attach(serve({ fetch: app.fetch, port: 3000 }))
app.allによるキャッチオールは通常のルートであり、独自のHonoミドルウェアを作成する際と同じスコーピングの考え方です。c.req.rawは、基盤となるWeb StandardのRequestです。fetchハンドラーは個々のリクエストしか受け取らず、実行中のdevframeはポートをバインドしないため、attach()を使ってNodeサーバーのアップグレードイベントをdevframeのソケットに引き渡します。この呼び出しがないと、UIは読み込まれてもライブRPCが接続されません。
その他のホストとパッケージ化されたアダプター
Devframeのホストはすべて1つのルールに従います。Web Standardのホストはhandlerを、Connectスタイルの開発サーバーはnodeMiddlewareをマウントし、ルートハンドラーがアップグレードリクエストを受け取れないホストではサイドカーWebSocketを使用します。
| ホスト | マウント方法 | ルート / ファイル | WebSocket |
|---|---|---|---|
| Vite | server.middlewares.use経由でnodeMiddleware | configureServerプラグイン | server: server.httpServer |
| Hono | handler(c.req.raw) | app.all('/__my-tool/*', …) | devtools.attach(serve(...)) |
| Nitro | handler(event.req) | routes/__my-tool/[...path].tsおよびroutes/__my-tool/index.ts | ws: { sidecar: true } |
| Next.js | export const GET = devtools.handler | app/%5F_my-tool/[[...path]]/route.ts | ws: { sidecar: true } |
| SvelteKit | GET = ({ request }) => devtools.handler(request) | src/routes/%5F_my-tool/[...path]/+server.ts | ws: { sidecar: true } |
Next.jsでは、アンダースコアで始まるフォルダーはプライベートとして扱われます。そのため、/__my-tool/で配信するルートは%5F_my-toolという名前のフォルダーに配置します。この名前はデコードすると__my-toolになります。Next.js、Nitro、SvelteKitのように開発中にモジュールをリロードするホストでは、リロードのたびに新しいdevframeが作成されないよう、インスタンスをglobalThis上にメモ化してください。initiateのページでは、このメモがReturnType<typeof initDevframe>として型付けされた例が示されています。
同じ定義は、ホストなしでパッケージ化することもできます。以下はいずれもアダプター一覧に掲載されています。
- スタンドアロンCLI:
devframe/adapters/cacのcreateCac()。オプションのpeer依存であるcacが必要です。 - 開発サーバー:
devframe/adapters/devのcreateDevServer()。 - 静的ビルド:
devframe/adapters/buildのcreateBuild()。スナップショットレポート向けです。 - MCPサーバー:
devframe/adapters/mcpのcreateMcpServer()。コーディングエージェント向けです。RPC関数は、明示的にオプトインしない限りエージェントには公開されません。
複数のホストに対応した動作するリファレンスプロジェクトが、リポジトリのexamples/フォルダーにあります。
認証ゲートは有効のままにする
Devframeはデフォルトでアクセスを制限します。未認可のブラウザーがアクセスを要求すると、ターミナルにコードとマジックリンクが表示されます。Devframeは、マジックリンクのオリジンを生のHostヘッダーやフォワードヘッダーから導出することはありません。そのため、ループバック以外のデプロイではoriginを明示的に設定する必要があります。auth: falseも用意されていますが、適切なのはlocalhost上の単一ユーザー環境に限られます。
Devframeが適しているケース、拡張機能のほうがシンプルなケース
Devframeが真価を発揮するのは、ツールがサーバーサイドの状態を必要とし、かつ複数のホストで動作させる必要がある場合です。単一のフレームワークでページを検査するだけのツールであれば、ブラウザー拡張機能のほうが小規模な実装で済みます。
Devframeを選ぶべきケース:
- ビルドグラフ、設定、プロセス環境、ファイルシステムなど、Node側の状態を読み取るツールである
- ライブラリが複数のフレームワークや開発サーバーで使われている
- 同じツールをCLI、CI向けの静的レポート、MCPサーバーとしても提供したい
ブラウザー拡張機能を選ぶべきケース:
- DOMやクライアントサイドのストアなど、すでにページ内に存在するものを検査するだけである
- 単一フレームワークのランタイムのみを対象としている
- 取得すべきサーバーサイドのデータがない
まとめ
Devframeを使えば、クロスフレームワーク対応はホストごとに数行のマウントコードを書くだけで実現できます。ツールを定義として一度だけ記述すれば、initDevframe()がそれを標準ハンドラーに変換し、各ホストはそのハンドラーを自身のルーターとWebSocketに接続するだけです。次のステップとして、Devframe定義ガイドを参考に必須メタデータとブラウザーエントリーを追加して定義を完成させましょう。そのうえで、ユーザーが最も多く利用しているホストにマウントし、表を参考に他のホストにも対応を広げてください。
よくある質問
DevframeのRPC関数をコーディングエージェントに公開するにはどうすればよいですか?
defineRpcFunctionに、descriptionを含むagentフィールドを追加します。例:agent: { description: 'Returns a summary' }。MCPはJSON形式のデータしか受け付けないため、agentフィールドを指定すると厳格なJSONシリアライズが有効になります。jsonSerializable: falseと組み合わせるとエラーDF0019がスローされます。定義はdevframe/adapters/mcpのcreateMcpServer(devframe, { transport: 'stdio' })で配信します。これにはオプションのpeer依存である@devframes/agenticが必要です。agentフィールドを持たない関数は、内部RPC専用のままとなります。
DevframeはViteなしで、あるいはCommonJSプロジェクトで使えますか?
はい。DevframeはViteに依存していないため、HonoのようなWeb Standardのホストであればどこでも動作し、createDevServerやcreateCacを使ってスタンドアロンツールとして実行することもできます。ESM専用で配布されているため、CommonJSのコードベースではESMのimportまたは動的なimport()呼び出しで読み込む必要があります。オプションのpeer依存であるcacをインストールせずにCLIアダプターを使用すると、インポート時にエラーが発生します。
Devframeはstatic以外にどのようなRPC関数タイプをサポートしていますか?
DevframeのRPC関数には、query、static、action、eventの4つのタイプがあります。事前計算された結果を保持するdumpフィールドを持てるのは、staticとqueryの関数だけです。actionやevent関数にdumpを追加すると、バリデーションエラーがスローされます。argsで宣言した引数は、ハンドラーの実行前にValibotなどの任意のStandard Schemaバリデーターで検証されます。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k