12k
All articles

Devframeでクロスフレームワーク対応の開発者ツールを構築する

Devframeでクロスフレームワーク開発ツールを構築。ツールを一度定義し、標準ハンドラーとWebSocketをVite、Hono、Next.jsなどに組み込みます。

OpenReplay Team
OpenReplay Team
Devframeでクロスフレームワーク対応の開発者ツールを構築する

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
Viteserver.middlewares.use経由でnodeMiddlewareconfigureServerプラグインserver: server.httpServer
Honohandler(c.req.raw)app.all('/__my-tool/*', …)devtools.attach(serve(...))
Nitrohandler(event.req)routes/__my-tool/[...path].tsおよびroutes/__my-tool/index.tsws: { sidecar: true }
Next.jsexport const GET = devtools.handlerapp/%5F_my-tool/[[...path]]/route.tsws: { sidecar: true }
SvelteKitGET = ({ request }) => devtools.handler(request)src/routes/%5F_my-tool/[...path]/+server.tsws: { 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バリデーターで検証されます。

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.