12k
All articles

Framework-übergreifende Entwicklerwerkzeuge mit Devframe entwickeln

Entwickeln Sie frameworkübergreifende Tools mit Devframe: Definieren Sie ein Tool einmal und binden Sie Standard-Handler und WebSocket in Vite, Hono, Next.js und mehr ein.

OpenReplay Team
OpenReplay Team
Framework-übergreifende Entwicklerwerkzeuge mit Devframe entwickeln

Devframe ist eine MIT-lizenzierte, framework-neutrale Grundlage für Entwicklerwerkzeuge: Sie beschreiben ein Tool einmalig mit defineDevframe(), und initDevframe() macht aus dieser Definition einen Request-Handler nach Web-Standard, den jeder kompatible Host einbinden kann.

Wenn Sie eine Library pflegen, haben Sie sich vermutlich schon einmal ein Inspector-Panel dafür gewünscht, etwa im Stil der React Developer Tools, nur eben für die Interna Ihrer eigenen Library. Dann kommt die Support-Matrix ins Spiel: ein Vite-Plugin, eine Next.js-Route, eine Hono-Middleware, und jede davon braucht ihren eigenen Server-Glue-Code.

In diesem Artikel entwickeln wir ein kleines Devframe-Tool, machen daraus einen Handler, binden dieselbe Definition in Vite und in Hono ein und geben anschließend einen Überblick über die übrigen Hosts und die vorgefertigten Adapter. Zum Schluss geht es um die Fälle, in denen eine Browser-Erweiterung die einfachere Wahl ist.

Das Wichtigste in Kürze

  • defineDevframe() aus devframe beschreibt ein Tool einmalig. initDevframe(def, { base }) aus devframe/initiate macht daraus eine laufende Instanz, die handler und nodeMiddleware bereitstellt.
  • Hono, Nitro, Next.js und SvelteKit binden devtools.handler ein, der einen Web-Standard-Request entgegennimmt und eine Response zurückgibt. Vite und Rsbuild binden devtools.nodeMiddleware ein.
  • Ein laufendes Devframe öffnet keinen eigenen Port. Der Host entscheidet, wo der RPC-WebSocket liegt: auf einem gemeinsam genutzten Server, über attach() oder über ws: { sidecar: true }.
  • Devframe schützt den Zugriff standardmäßig. auth: false ist nur für einen einzelnen Nutzer auf localhost angemessen.
  • Next.js und SvelteKit liefern /__my-tool/ aus einem Routenordner namens %5F_my-tool aus, und beide benötigen einen Side-Car-WebSocket.

Welches Problem löst Devframe?

Devframe löst das Problem „ein Plugin pro Framework“: Ohne eine gemeinsame Schicht muss jedes Devtool für jedes Host-Framework eine eigene Integration mitliefern. Devframe beseitigt diese Duplizierung, indem es einen einzigen Standard-Handler zwischen das Tool und seine Umgebung setzt. Die Ankündigung zu v1.0 betrachtet den Handler als die Außengrenze des Tools. Die UI, die Verbindungsdetails, Live-RPC, Authentifizierung und ein optionaler MCP-Endpunkt liegen alle dahinter, unter einem einzigen Pfadpräfix.

Da der Handler ausschließlich mit Request und Response arbeitet, hängt das Tool nicht mehr von der API eines bestimmten Dev-Servers ab. Mit dem Release v1.0 wurde die öffentliche Schnittstelle stabilisiert; der Quellcode liegt im Repository devframes/devframe unter der MIT-Lizenz.

Wie definiert man ein Devframe-Tool?

Eine Devframe-Definition ist ein einfaches Objekt, das von defineDevframe() zurückgegeben wird. Sie benennt das Tool und registriert dessen RPC-Funktionen innerhalb von setup(ctx). Das Beispiel-Tool listet die Namen von Umgebungsvariablen auf, niemals deren Werte.

Installationsanleitungen finden Sie in der Devframe-Dokumentation. Beginnen Sie mit der Definition:

// 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)
  },
})

Exportieren Sie in rpc.ts die Funktion listEnvNames als RPC-Funktion, deren Handler Object.keys(process.env).sort() zurückgibt. Die Struktur des Wrappers und seine Optionen werden im Devframe Definition Guide behandelt. Dort sind auch die Felder aufgeführt, die eine vollständige Definition benötigt: id, name, version, packageName, homepage, description und setup sind Pflichtfelder, zusätzlich sollten Sie importMetaUrl: import.meta.url übergeben. Der Guide erklärt außerdem, dass ein gescopter Kontext die RPC-IDs mit der id des Devframes als Namespace versieht.

Dass nur Namen zurückgegeben werden, ist eine bewusste Entscheidung in Bezug auf die Datenexposition. Das Auth-Gate schützt zwar das Panel, aber eine Funktion, die niemals Werte ausliest, kann auch keine preisgeben – ganz gleich, wer das Panel am Ende öffnet.

Die Definition in einen Handler umwandeln

initDevframe() wandelt eine Definition in eine laufende Instanz um, deren handler das gesamte Tool unter einem einzigen Basispfad ausliefert. Übergeben Sie die Definition und eine base:

// devtools.ts
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'

export const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })

Die Dokumentation zum Standard-Handler erläutert, dass ein einziger Mount-Basispfad das gesamte Tool abdeckt: Der Handler liefert die SPA, die Discovery-Datei __connection.json, den RPC-Socket, das Auth-Gate und die MCP-Route aus. Die Option base ist verpflichtend. Sowohl handler als auch nodeMiddleware warten intern auf die Einsatzbereitschaft, Sie können sie also sofort einbinden. Die Instanz stellt außerdem attach, handleUpgrade und close() bereit, um den Socket und dessen Lebenszyklus zu steuern.

Einbindung in Vite

Um ein Devframe-Tool in Vite einzubinden, verwenden Sie devtools.nodeMiddleware in einem configureServer-Hook, da Vite mit Middleware im Connect-Stil arbeitet. Erzeugen Sie die Instanz innerhalb des Hooks, damit sie den HTTP-Server von Vite mitnutzen kann:

// 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' hält das Panel aus Produktions-Builds heraus. Die Option server legt den RPC-WebSocket auf den HTTP-Server von Vite, sodass kein zusätzlicher Port geöffnet wird. Rsbuild nimmt nodeMiddleware auf dieselbe Weise entgegen, seine Handler bekommen jedoch nie Upgrade-Requests zu sehen; daher benötigt Rsbuild für den Socket ws: { sidecar: true }.

Dasselbe Tool in Hono einbinden

Hono verarbeitet Web-Standard-Requests nativ. Übergeben Sie daher den rohen Request an devtools.handler und hängen Sie den WebSocket an den Node-Server. Dabei wird das Modul devtools.ts von oben wiederverwendet. Nur der Glue-Code ändert sich:

// 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 }))

Die Catch-all-Route app.all ist eine gewöhnliche Route – dasselbe Scoping-Prinzip, das auch beim Schreiben eigener Hono-Middleware zum Einsatz kommt. c.req.raw ist der zugrunde liegende Web-Standard-Request. Ein Fetch-Handler sieht immer nur einzelne Requests, und ein laufendes Devframe bindet keinen Port. Deshalb übergibt attach() die Upgrade-Events des Node-Servers an den Devframe-Socket. Ohne diesen Aufruf lädt zwar die UI, aber Live-RPC hat keine Verbindung.

Weitere Hosts und vorgefertigte Adapter

Für alle Devframe-Hosts gilt eine Regel: Web-Standard-Hosts binden handler ein, Dev-Server im Connect-Stil binden nodeMiddleware ein, und Hosts, deren Route-Handler nie Upgrade-Requests erhalten, verwenden einen Side-Car-WebSocket.

HostEinbindungRoute / DateiWebSocket
VitenodeMiddleware über server.middlewares.useconfigureServer-Pluginserver: server.httpServer
Honohandler(c.req.raw)app.all('/__my-tool/*', …)devtools.attach(serve(...))
Nitrohandler(event.req)routes/__my-tool/[...path].ts sowie 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 behandelt Ordner, die mit einem Unterstrich beginnen, als privat. Eine Route, die unter /__my-tool/ ausgeliefert wird, liegt daher in einem Ordner namens %5F_my-tool, was dekodiert wieder __my-tool ergibt. Hosts, die Module während der Entwicklung neu laden – etwa Next.js, Nitro und SvelteKit –, sollten die Instanz auf globalThis memoisieren, damit nicht bei jedem Reload ein neues Devframe entsteht. Die Initiate-Seite zeigt dieses Memo mit dem Typ ReturnType<typeof initDevframe>.

Dieselbe Definition lässt sich auch ohne Host paketieren. Alle folgenden Varianten sind im Adapter-Index aufgeführt:

  • Standalone-CLI: createCac() aus devframe/adapters/cac. Erfordert die optionale Peer-Dependency cac.
  • Dev-Server: createDevServer() aus devframe/adapters/dev.
  • Statischer Build: createBuild() aus devframe/adapters/build, für Snapshot-Reports.
  • MCP-Server: createMcpServer() aus devframe/adapters/mcp, für Coding-Agents. RPC-Funktionen bleiben für Agents unsichtbar, sofern sie nicht explizit freigegeben werden (Opt-in).

Lauffähige Referenzprojekte für mehrere Hosts finden Sie im examples/-Ordner des Repositorys.

Lassen Sie das Auth-Gate aktiviert

Devframe schützt den Zugriff standardmäßig. Fordert ein noch nicht autorisierter Browser Zugriff an, gibt das Terminal einen Code und einen Magic Link aus. Devframe leitet den Origin des Magic Links niemals aus einem rohen Host-Header oder aus Forwarded-Headern ab; jedes Deployment, das nicht auf Loopback läuft, muss origin daher explizit setzen. auth: false existiert zwar, ist aber nur für einen einzelnen Nutzer auf localhost angemessen.

Wann lohnt sich Devframe, und wann ist eine Erweiterung einfacher?

Devframe zahlt sich aus, wenn ein Tool serverseitigen State benötigt und in mehr als einem Host laufen muss. Wenn das Tool lediglich die Seite in einem einzigen Framework inspiziert, ist eine Browser-Erweiterung der schlankere Weg.

Wählen Sie Devframe, wenn:

  • das Tool Node-seitigen State ausliest, etwa den Build-Graphen, die Konfiguration, die Prozessumgebung oder das Dateisystem;
  • die Library in mehreren Frameworks oder Dev-Servern eingesetzt wird;
  • Sie dasselbe Tool als CLI, als statischen CI-Report oder als MCP-Server ausliefern möchten.

Wählen Sie eine Browser-Erweiterung, wenn:

  • das Tool nur inspiziert, was ohnehin schon in der Seite vorhanden ist, etwa das DOM oder einen clientseitigen Store;
  • es auf die Runtime eines einzelnen Frameworks abzielt;
  • keine serverseitigen Daten abgerufen werden müssen.

Fazit

Mit Devframe reduziert sich framework-übergreifender Support auf wenige Zeilen Mount-Code pro Host. Sie schreiben das Tool einmal als Definition, initDevframe() macht daraus einen Standard-Handler, und jeder Host verbindet diesen Handler mit seinem Router und seinem WebSocket. Vervollständigen Sie als Nächstes die Definition mit den erforderlichen Metadaten und einem Browser-Entry gemäß dem Devframe Definition Guide, binden Sie sie dann in den Host ein, den Ihre Nutzer am häufigsten verwenden, und ergänzen Sie die übrigen anhand der Tabelle.

FAQs

Wie stelle ich eine Devframe-RPC-Funktion einem Coding-Agent zur Verfügung?

Fügen Sie defineRpcFunction ein agent-Feld mit einer Beschreibung hinzu, zum Beispiel agent: { description: 'Returns a summary' }. Das agent-Feld aktiviert eine strikte JSON-Serialisierung, da MCP nur JSON-förmige Daten akzeptiert; in Kombination mit jsonSerializable: false wird der Fehler DF0019 ausgelöst. Stellen Sie die Definition mit createMcpServer(devframe, { transport: 'stdio' }) aus devframe/adapters/mcp bereit, wofür die optionale Peer-Dependency @devframes/agentic benötigt wird. Funktionen ohne agent-Feld bleiben rein interne RPC-Funktionen.

Kann ich Devframe ohne Vite oder in einem CommonJS-Projekt verwenden?

Ja. Devframe hat keine Abhängigkeit von Vite und läuft daher in jedem Web-Standard-Host wie Hono oder als eigenständiges Tool über createDevServer oder createCac. Devframe wird ausschließlich als ESM ausgeliefert; eine CommonJS-Codebasis muss es daher über ESM-Imports oder einen dynamischen import()-Aufruf laden. Wenn Sie den CLI-Adapter verwenden, ohne die optionale Peer-Dependency cac zu installieren, tritt der Fehler bereits beim Import auf.

Welche RPC-Funktionstypen unterstützt Devframe neben static?

Devframe-RPC-Funktionen verwenden einen von vier Typen: query, static, action und event. Nur static- und query-Funktionen können ein dump-Feld für vorberechnete Ergebnisse enthalten. Wird dump zu einer action- oder event-Funktion hinzugefügt, wird ein Validierungsfehler ausgelöst. In args deklarierte Argumente werden vor der Ausführung des Handlers gegen einen beliebigen Standard-Schema-Validator wie Valibot geprüft.

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.