12k
All articles

Cómo crear herramientas para desarrolladores multiframework con Devframe

Crea herramientas de desarrollo multiplataforma con Devframe: define una herramienta una vez e integra su manejador estándar y WebSocket en Vite, Hono, Next.js y más.

OpenReplay Team
OpenReplay Team
Cómo crear herramientas para desarrolladores multiframework con Devframe

Devframe es una base para herramientas de desarrollo, con licencia MIT e independiente del framework: describes una herramienta una sola vez con defineDevframe(), e initDevframe() convierte esa definición en un request handler basado en Web Standards que cualquier host compatible puede montar.

Si mantienes una librería, probablemente hayas querido un panel de inspección para ella, algo parecido a las React Developer Tools, pero para tu propio funcionamiento interno. Luego llega la matriz de compatibilidad: un plugin de Vite, una ruta de Next.js, un middleware de Hono, y cada uno necesita su propio código de integración en el servidor.

En este artículo construiremos una pequeña herramienta con Devframe, la convertiremos en un handler, montaremos la misma definición en Vite y en Hono, y después repasaremos el resto de hosts y los adaptadores empaquetados. Para terminar, veremos los casos en los que una extensión de navegador es la opción más sencilla.

Puntos clave

  • defineDevframe(), de devframe, describe una herramienta una sola vez. initDevframe(def, { base }), de devframe/initiate, la convierte en una instancia en ejecución que expone handler y nodeMiddleware.
  • Hono, Nitro, Next.js y SvelteKit montan devtools.handler, que recibe un Request de Web Standards y devuelve un Response. Vite y Rsbuild montan devtools.nodeMiddleware.
  • Una instancia de devframe en ejecución no abre ningún puerto propio. Es el host quien decide dónde reside el WebSocket de RPC: un servidor compartido, attach() o ws: { sidecar: true }.
  • Devframe restringe el acceso por defecto. auth: false solo es apropiado para un único usuario en localhost.
  • Next.js y SvelteKit sirven /__my-tool/ desde una carpeta de rutas llamada %5F_my-tool, y ambos necesitan un WebSocket side-car.

¿Qué problema resuelve Devframe?

Devframe resuelve el problema de tener un plugin por cada framework: sin una capa compartida, cada devtool tiene que publicar una integración distinta para cada framework host. Devframe elimina esa duplicación colocando un único handler estándar entre la herramienta y su entorno. El anuncio de la v1.0 trata el handler como la frontera de la herramienta. La UI, los datos de conexión, el RPC en tiempo real, la autenticación y un endpoint MCP opcional quedan todos detrás de él, bajo un único prefijo de ruta.

Como el handler solo trabaja con Request y Response, la herramienta ya no depende de la API de ningún servidor de desarrollo concreto. La versión v1.0 estabilizó la interfaz pública, y el código fuente se encuentra en el repositorio devframes/devframe bajo licencia MIT.

¿Cómo se define una herramienta con Devframe?

Una definición de Devframe es un objeto plano devuelto por defineDevframe(). Asigna un nombre a la herramienta y registra sus funciones RPC dentro de setup(ctx). La herramienta de ejemplo lista los nombres de las variables de entorno, nunca sus valores.

Las instrucciones de instalación están en la documentación de Devframe. Empieza por la definición:

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

En rpc.ts, exporta listEnvNames como una función RPC cuyo handler devuelva Object.keys(process.env).sort(). La estructura del wrapper y sus opciones se explican en la guía de definición de Devframe. Esa guía también enumera los campos que necesita una definición completa: id, name, version, packageName, homepage, description y setup son obligatorios, y además conviene pasar importMetaUrl: import.meta.url. También explica que un contexto con scope asigna a los ids de RPC un namespace basado en el id del devframe.

Devolver solo los nombres es una decisión deliberada sobre la exposición de datos. La autenticación protege el panel, pero una función que nunca lee los valores no puede filtrarlos, sea quien sea quien acabe abriendo el panel.

Convertir la definición en un handler

initDevframe() convierte una definición en una instancia activa cuyo handler sirve toda la herramienta bajo una única ruta base. Pasa la definición y un base:

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

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

La documentación del Standard Handler explica que una sola base de montaje cubre toda la herramienta: el handler sirve la SPA, el archivo de descubrimiento __connection.json, el socket de RPC, la puerta de autenticación y la ruta MCP. La opción base es obligatoria. Tanto handler como nodeMiddleware esperan internamente a que la instancia esté lista, así que puedes montarlos de inmediato. La instancia también expone attach, handleUpgrade y close() para controlar el socket y su ciclo de vida.

Montarlo en Vite

Para montar una herramienta de Devframe en Vite, usa devtools.nodeMiddleware desde un hook configureServer, ya que Vite trabaja con middleware al estilo Connect. Crea la instancia dentro del hook para que pueda compartir el servidor 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' mantiene el panel fuera de los builds de producción. La opción server coloca el WebSocket de RPC en el propio servidor HTTP de Vite, de modo que no se abre ningún puerto adicional. Rsbuild acepta nodeMiddleware de la misma forma, pero sus handlers nunca reciben las peticiones de upgrade, por lo que necesita ws: { sidecar: true } para el socket.

Montar la misma herramienta en Hono

Hono gestiona de forma nativa las peticiones basadas en Web Standards, así que basta con pasar la petición sin procesar a devtools.handler y conectar el WebSocket al servidor de Node. Aquí se reutiliza el módulo devtools.ts de antes. Solo cambia el código de integración:

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

El catch-all app.all es una ruta normal, con la misma idea de acotación que se usa al escribir tu propio middleware para Hono. c.req.raw es el Request de Web Standards subyacente. Un fetch handler solo ve peticiones individuales, y una instancia de devframe en ejecución no ocupa ningún puerto, así que attach() entrega los eventos de upgrade del servidor de Node al socket de devframe. Sin esa llamada, la UI se carga, pero el RPC en tiempo real no tiene conexión.

Otros hosts y adaptadores empaquetados

Todos los hosts de Devframe siguen una misma regla: los hosts basados en Web Standards montan handler, los servidores de desarrollo al estilo Connect montan nodeMiddleware, y los hosts cuyos route handlers nunca reciben peticiones de upgrade usan un WebSocket side-car.

HostMontajeRuta / archivoWebSocket
VitenodeMiddleware mediante server.middlewares.useplugin con configureServerserver: server.httpServer
Honohandler(c.req.raw)app.all('/__my-tool/*', …)devtools.attach(serve(...))
Nitrohandler(event.req)routes/__my-tool/[...path].ts junto con 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 trata como privadas las carpetas que empiezan por guion bajo. Por eso, una ruta servida en /__my-tool/ reside en una carpeta llamada %5F_my-tool, que se decodifica de nuevo como __my-tool. Los hosts que recargan módulos durante el desarrollo, como Next.js, Nitro y SvelteKit, deberían memoizar la instancia en globalThis para que cada recarga no cree un nuevo devframe. La página de initiate muestra esa memo tipada como ReturnType<typeof initDevframe>.

La misma definición también puede empaquetarse sin necesidad de un host. Todas estas opciones aparecen en el índice de adaptadores:

  • CLI independiente: createCac() de devframe/adapters/cac. Requiere la dependencia peer opcional cac.
  • Servidor de desarrollo: createDevServer() de devframe/adapters/dev.
  • Build estático: createBuild() de devframe/adapters/build, para informes de tipo snapshot.
  • Servidor MCP: createMcpServer() de devframe/adapters/mcp, para agentes de programación. Las funciones RPC no son accesibles para los agentes salvo que se habilite explícitamente.

En la carpeta examples/ del repositorio hay proyectos de referencia funcionales para varios hosts.

Mantén activa la autenticación

Devframe restringe el acceso por defecto. Cuando un navegador no autorizado solicita acceso, la terminal muestra un código y un magic link. Devframe nunca deduce el origen del magic link a partir de una cabecera Host sin procesar ni de cabeceras reenviadas, así que cualquier despliegue que no esté en loopback debe definir origin de forma explícita. auth: false existe, pero solo es apropiado para un único usuario en localhost.

¿Cuándo merece la pena Devframe y cuándo es más sencilla una extensión?

Devframe compensa cuando una herramienta necesita estado del lado del servidor y tiene que funcionar en más de un host. Si la herramienta solo inspecciona la página en un único framework, una extensión de navegador supone un desarrollo más ligero.

Elige Devframe cuando:

  • la herramienta lee estado del lado de Node, como el grafo de build, la configuración, el entorno del proceso o el sistema de archivos;
  • la librería se usa en varios frameworks o servidores de desarrollo;
  • quieres distribuir la misma herramienta como CLI, como informe estático de CI o como servidor MCP.

Elige una extensión de navegador cuando:

  • la herramienta solo inspecciona lo que ya está en la página, como el DOM o un store del lado del cliente;
  • está orientada al runtime de un único framework;
  • no hay datos del lado del servidor que obtener.

Conclusión

Con Devframe, el soporte multiframework se reduce a unas pocas líneas de código de montaje por host. Escribes la herramienta una sola vez como definición, initDevframe() la convierte en un handler estándar, y cada host conecta ese handler a su router y a su WebSocket. Como siguiente paso, completa la definición con los metadatos obligatorios y un punto de entrada para el navegador siguiendo la guía de definición de Devframe; después, móntala en el host que más usen tus usuarios y añade los demás a partir de la tabla.

Preguntas frecuentes

¿Cómo expongo una función RPC de Devframe a un agente de programación?

Añade un campo agent con una descripción a defineRpcFunction, por ejemplo agent: { description: 'Returns a summary' }. El campo agent activa la serialización JSON estricta, porque MCP solo acepta datos con estructura JSON, y combinarlo con jsonSerializable: false lanza el error DF0019. Sirve la definición con createMcpServer(devframe, { transport: 'stdio' }) de devframe/adapters/mcp, que requiere la dependencia peer opcional @devframes/agentic. Las funciones sin campo agent siguen siendo exclusivamente RPC interno.

¿Puedo usar Devframe sin Vite o en un proyecto CommonJS?

Sí. Devframe no depende de Vite, así que funciona en cualquier host basado en Web Standards, como Hono, o como herramienta independiente mediante createDevServer o createCac. Se distribuye solo como ESM, por lo que un código base CommonJS tiene que cargarlo con imports ESM o con una llamada dinámica a import(). Si usas el adaptador de CLI sin instalar su dependencia peer opcional cac, el error aparece en el momento de la importación.

¿Qué tipos de funciones RPC admite Devframe además de static?

Las funciones RPC de Devframe usan uno de estos cuatro tipos: query, static, action y event. Solo las funciones static y query pueden incluir un campo dump con resultados precalculados. Añadir dump a una función action o event lanza un error de validación. Los argumentos declarados en args se validan con cualquier validador compatible con Standard Schema, como Valibot, antes de que se ejecute el handler.

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.