Créer des outils de développement multi-frameworks avec Devframe
Créez des outils de développement multiplateformes avec Devframe : définissez un outil une fois, puis intégrez son gestionnaire standard et son WebSocket à Vite, Hono, Next.js et plus.
Devframe est une fondation pour outils de développement, sous licence MIT et indépendante de tout framework : vous décrivez un outil une seule fois avec defineDevframe(), et initDevframe() transforme cette définition en un gestionnaire de requêtes conforme aux Web Standards, que tout hôte compatible peut monter.
Si vous maintenez une bibliothèque, vous avez sans doute déjà souhaité lui adjoindre un panneau d’inspection, dans l’esprit des React Developer Tools, mais pour vos propres mécanismes internes. Puis vient la matrice de compatibilité : un plugin Vite, une route Next.js, un middleware Hono, et chacun exige son propre code d’intégration côté serveur.
Cet article construit un petit outil Devframe, le transforme en gestionnaire, monte la même définition dans Vite puis dans Hono, et présente ensuite les autres hôtes ainsi que les adaptateurs prêts à l’emploi. Il se termine par les cas où une extension de navigateur reste la solution la plus simple.
Points clés à retenir
defineDevframe(), issu dedevframe, décrit un outil une seule fois.initDevframe(def, { base }), issu dedevframe/initiate, le transforme en une instance active qui exposehandleretnodeMiddleware.- Hono, Nitro, Next.js et SvelteKit montent
devtools.handler, qui reçoit uneRequestconforme aux Web Standards et renvoie uneResponse. Vite et Rsbuild montentdevtools.nodeMiddleware. - Une instance devframe en cours d’exécution n’ouvre aucun port qui lui soit propre. C’est l’hôte qui détermine où réside le WebSocket RPC : un serveur partagé,
attach()ouws: { sidecar: true }. - Devframe contrôle l’accès par défaut.
auth: falsene convient qu’à un utilisateur unique sur localhost. - Next.js et SvelteKit servent
/__my-tool/depuis un dossier de route nommé%5F_my-tool, et tous deux nécessitent un WebSocket side-car.
Quel problème Devframe résout-il ?
Devframe résout le problème du « un plugin par framework » : sans couche commune, chaque outil de développement doit fournir une intégration distincte pour chaque framework hôte. Devframe élimine cette duplication en plaçant un gestionnaire standard unique entre l’outil et son environnement. L’annonce de la v1.0 considère ce gestionnaire comme la frontière de l’outil. L’interface utilisateur, les informations de connexion, le RPC en temps réel, l’authentification et un endpoint MCP optionnel se trouvent tous derrière lui, sous un préfixe de chemin unique.
Comme le gestionnaire ne manipule que des objets Request et Response, l’outil ne dépend plus de l’API d’un serveur de développement particulier. La version 1.0 a stabilisé l’interface publique, et le code source est disponible dans le dépôt devframes/devframe sous licence MIT.
Comment définir un outil Devframe ?
Une définition Devframe est un simple objet renvoyé par defineDevframe(). Elle nomme l’outil et enregistre ses fonctions RPC dans setup(ctx). L’outil d’exemple liste les noms des variables d’environnement, jamais leurs valeurs.
Les instructions d’installation figurent dans la documentation de Devframe. Commencez par la définition :
// 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)
},
})
Dans rpc.ts, exportez listEnvNames sous la forme d’une fonction RPC dont le gestionnaire renvoie Object.keys(process.env).sort(). La structure du wrapper et ses options sont détaillées dans le guide de définition Devframe. Ce guide liste également les champs nécessaires à une définition complète : id, name, version, packageName, homepage, description et setup sont obligatoires, et il est recommandé de passer aussi importMetaUrl: import.meta.url. Il précise en outre qu’un contexte à portée (scoped) préfixe les identifiants RPC avec l’id du devframe.
Ne renvoyer que les noms est un choix délibéré en matière d’exposition des données. Le contrôle d’accès protège le panneau, mais une fonction qui ne lit jamais les valeurs ne peut pas les divulguer, quelle que soit la personne qui finit par ouvrir le panneau.
Transformer la définition en gestionnaire
initDevframe() convertit une définition en une instance active dont le handler sert l’ensemble de l’outil sous un chemin de base unique. Passez-lui la définition et une base :
// devtools.ts
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'
export const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
La documentation du gestionnaire standard explique qu’une seule base de montage couvre l’ensemble de l’outil : le gestionnaire sert la SPA, le fichier de découverte __connection.json, le socket RPC, le contrôle d’accès et la route MCP. L’option base est obligatoire. handler et nodeMiddleware attendent tous deux en interne que l’instance soit prête : vous pouvez donc les monter immédiatement. L’instance expose également attach, handleUpgrade et close() pour piloter le socket et son cycle de vie.
Le monter dans Vite
Pour monter un outil Devframe dans Vite, utilisez devtools.nodeMiddleware depuis un hook configureServer, car Vite repose sur des middlewares de type Connect. Créez l’instance à l’intérieur du hook afin qu’elle puisse partager le serveur HTTP de Vite :
// 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' exclut le panneau des builds de production. L’option server place le WebSocket RPC sur le serveur HTTP de Vite lui-même, si bien qu’aucun port supplémentaire n’est ouvert. Rsbuild accepte nodeMiddleware de la même manière, mais ses gestionnaires ne reçoivent jamais les requêtes d’upgrade : il a donc besoin de ws: { sidecar: true } pour le socket.
Monter le même outil dans Hono
Hono traite nativement les requêtes conformes aux Web Standards : il suffit donc de transmettre la requête brute à devtools.handler et de rattacher le WebSocket au serveur Node. On réutilise ici le module devtools.ts vu précédemment. Seul le code d’intégration change :
// 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 }))
Le catch-all app.all est une route ordinaire, selon le même principe de délimitation que celui employé pour écrire son propre middleware Hono. c.req.raw correspond à la Request Web Standard sous-jacente. Un gestionnaire fetch ne voit jamais que des requêtes individuelles, et une instance devframe active n’écoute sur aucun port : attach() transmet donc les événements d’upgrade du serveur Node au socket devframe. Sans cet appel, l’interface se charge, mais le RPC en temps réel ne dispose d’aucune connexion.
Autres hôtes et adaptateurs prêts à l’emploi
Tous les hôtes Devframe obéissent à une même règle : les hôtes Web Standard montent handler, les serveurs de développement de type Connect montent nodeMiddleware, et les hôtes dont les gestionnaires de routes ne reçoivent jamais les requêtes d’upgrade utilisent un WebSocket side-car.
| Hôte | Montage | Route / fichier | WebSocket |
|---|---|---|---|
| Vite | nodeMiddleware via server.middlewares.use | Plugin 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 et 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 considère les dossiers dont le nom commence par un underscore comme privés. Une route servie à l’adresse /__my-tool/ réside donc dans un dossier nommé %5F_my-tool, qui se décode en __my-tool. Les hôtes qui rechargent les modules pendant le développement, comme Next.js, Nitro et SvelteKit, doivent mémoïser l’instance sur globalThis afin que chaque rechargement ne crée pas un nouveau devframe. La page consacrée à initiate montre ce cache typé en ReturnType<typeof initDevframe>.
La même définition peut aussi être packagée sans hôte. Toutes ces options sont répertoriées dans l’index des adaptateurs :
- CLI autonome :
createCac()depuisdevframe/adapters/cac. Nécessite la dépendance peer optionnellecac. - Serveur de développement :
createDevServer()depuisdevframe/adapters/dev. - Build statique :
createBuild()depuisdevframe/adapters/build, pour des rapports instantanés (snapshots). - Serveur MCP :
createMcpServer()depuisdevframe/adapters/mcp, pour les agents de codage. Les fonctions RPC restent inaccessibles aux agents, sauf si elles y sont explicitement exposées.
Des projets de référence fonctionnels pour plusieurs hôtes sont disponibles dans le dossier examples/ du dépôt.
Laisser le contrôle d’accès activé
Devframe contrôle l’accès par défaut. Lorsqu’un navigateur non autorisé demande l’accès, le terminal affiche un code et un lien magique. Devframe ne déduit jamais l’origine du lien magique d’un en-tête Host brut ni d’en-têtes de transfert (forwarded headers) : tout déploiement qui ne s’exécute pas en loopback doit donc définir origin explicitement. L’option auth: false existe, mais elle ne convient qu’à un utilisateur unique sur localhost.
Quand Devframe est-il pertinent, et quand une extension est-elle plus simple ?
Devframe devient rentable lorsqu’un outil a besoin d’un état côté serveur et doit fonctionner dans plusieurs hôtes. Si l’outil se contente d’inspecter la page dans un seul framework, une extension de navigateur représente un développement plus léger.
Choisissez Devframe lorsque :
- l’outil lit un état côté Node, comme le graphe de build, la configuration, l’environnement du processus ou le système de fichiers ;
- la bibliothèque est utilisée avec plusieurs frameworks ou serveurs de développement ;
- vous souhaitez distribuer le même outil sous forme de CLI, de rapport statique en CI ou de serveur MCP.
Choisissez une extension de navigateur lorsque :
- l’outil se contente d’inspecter ce qui est déjà présent dans la page, comme le DOM ou un store côté client ;
- il cible le runtime d’un seul framework ;
- il n’y a aucune donnée côté serveur à récupérer.
Conclusion
Avec Devframe, la prise en charge de plusieurs frameworks se résume à quelques lignes de code de montage par hôte. Vous écrivez l’outil une seule fois sous forme de définition, initDevframe() le transforme en gestionnaire standard, et chaque hôte relie ce gestionnaire à son routeur et à son WebSocket. Prochaine étape : complétez la définition avec les métadonnées obligatoires et un point d’entrée navigateur en suivant le guide de définition Devframe, puis montez-la dans l’hôte le plus utilisé par vos utilisateurs, avant d’ajouter les autres à l’aide du tableau.
FAQ
Comment exposer une fonction RPC Devframe à un agent de codage ?
Ajoutez un champ agent accompagné d'une description à defineRpcFunction, par exemple agent: { description: 'Returns a summary' }. Le champ agent active une sérialisation JSON stricte, car MCP n'accepte que des données au format JSON ; le combiner avec jsonSerializable: false déclenche l'erreur DF0019. Servez la définition avec createMcpServer(devframe, { transport: 'stdio' }) depuis devframe/adapters/mcp, qui nécessite la dépendance peer optionnelle @devframes/agentic. Les fonctions dépourvues de champ agent restent des fonctions RPC purement internes.
Puis-je utiliser Devframe sans Vite ou dans un projet CommonJS ?
Oui. Devframe ne dépend pas de Vite : il fonctionne dans n'importe quel hôte Web Standard, comme Hono, ou en tant qu'outil autonome via createDevServer ou createCac. Il est distribué exclusivement au format ESM : une base de code CommonJS doit donc le charger avec des imports ESM ou un appel dynamique à import(). Si vous utilisez l'adaptateur CLI sans installer sa dépendance peer optionnelle cac, l'erreur survient au moment de l'import.
Quels types de fonctions RPC Devframe prend-il en charge en dehors de static ?
Les fonctions RPC Devframe relèvent de l'un des quatre types suivants : query, static, action et event. Seules les fonctions static et query peuvent comporter un champ dump pour des résultats précalculés. Ajouter dump à une fonction action ou event déclenche une erreur de validation. Les arguments déclarés dans args sont vérifiés à l'aide de n'importe quel validateur Standard Schema, comme Valibot, avant l'exécution du gestionnaire.
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