12k
All articles

Criando ferramentas de desenvolvimento multiframework com o Devframe

Crie ferramentas de desenvolvimento para vários frameworks com Devframe: defina uma vez e integre o handler padrão e o WebSocket ao Vite, Hono, Next.js e mais.

OpenReplay Team
OpenReplay Team
Criando ferramentas de desenvolvimento multiframework com o Devframe

O Devframe é uma base para ferramentas de desenvolvimento, com licença MIT e independente de framework: você descreve uma ferramenta uma única vez com defineDevframe(), e initDevframe() transforma essa definição em um request handler baseado em Web Standards que qualquer host compatível pode montar.

Se você mantém uma biblioteca, provavelmente já quis ter um painel de inspeção para ela, algo nos moldes do React Developer Tools, mas voltado para os seus próprios mecanismos internos. Aí chega a matriz de suporte: um plugin para Vite, uma rota para Next.js, um middleware para Hono, e cada um deles exige seu próprio código de integração com o servidor.

Este artigo cria uma pequena ferramenta com o Devframe, transforma essa ferramenta em um handler, monta a mesma definição no Vite e no Hono e, em seguida, apresenta os demais hosts e adapters empacotados. Para fechar, mostra os casos em que uma extensão de navegador é a opção mais simples.

Principais conclusões

  • defineDevframe(), do pacote devframe, descreve uma ferramenta uma única vez. initDevframe(def, { base }), de devframe/initiate, transforma essa definição em uma instância em execução que expõe handler e nodeMiddleware.
  • Hono, Nitro, Next.js e SvelteKit montam devtools.handler, que recebe um Request Web Standard e retorna um Response. Vite e Rsbuild montam devtools.nodeMiddleware.
  • Um devframe em execução não abre nenhuma porta própria. É o host que decide onde o WebSocket de RPC fica: em um servidor compartilhado, via attach() ou com ws: { sidecar: true }.
  • O Devframe controla o acesso por padrão. auth: false só é adequado para um único usuário em localhost.
  • Next.js e SvelteKit servem /__my-tool/ a partir de uma pasta de rota chamada %5F_my-tool, e ambos precisam de um WebSocket side-car.

Que problema o Devframe resolve?

O Devframe resolve o problema de “um plugin por framework”: sem uma camada compartilhada, cada devtool precisa distribuir uma integração separada para cada framework host. O Devframe elimina essa duplicação ao colocar um único handler padrão entre a ferramenta e o seu ambiente. O anúncio da v1.0 trata o handler como a fronteira da ferramenta. A UI, os detalhes de conexão, o RPC em tempo real, a autenticação e um endpoint MCP opcional ficam todos atrás dele, sob um único prefixo de caminho.

Como o handler lida apenas com Request e Response, a ferramenta deixa de depender da API de um dev server específico. A versão 1.0 estabilizou a interface pública, e o código-fonte está no repositório devframes/devframe, sob a licença MIT.

Como definir uma ferramenta com o Devframe?

Uma definição do Devframe é um objeto simples retornado por defineDevframe(). Ela dá um nome à ferramenta e registra suas funções RPC dentro de setup(ctx). A ferramenta de exemplo lista os nomes das variáveis de ambiente, e nunca os seus valores.

As instruções de instalação estão na documentação do Devframe. Comece pela definição:

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

Em rpc.ts, exporte listEnvNames como uma função RPC cujo handler retorna Object.keys(process.env).sort(). O formato do wrapper e suas opções são abordados no guia de definição do Devframe. Esse guia também lista os campos de que uma definição completa precisa: id, name, version, packageName, homepage, description e setup são obrigatórios, e você também deve passar importMetaUrl: import.meta.url. O guia explica ainda que um contexto com escopo aplica o id do devframe como namespace aos ids de RPC.

Retornar apenas os nomes é uma escolha deliberada de exposição de dados. O controle de autenticação protege o painel, mas uma função que nunca lê os valores não tem como vazá-los, seja quem for que acabe abrindo o painel.

Transforme a definição em um handler

initDevframe() converte uma definição em uma instância ativa cujo handler serve a ferramenta inteira sob um único caminho base. Passe a definição e um base:

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

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

A documentação do Standard Handler explica que um único caminho base de montagem cobre toda a ferramenta: o handler serve a SPA, o arquivo de descoberta __connection.json, o socket de RPC, o controle de autenticação e a rota MCP. A opção base é obrigatória. Tanto handler quanto nodeMiddleware aguardam internamente até a instância estar pronta, então você pode montá-los imediatamente. A instância também expõe attach, handleUpgrade e close() para controlar o socket e o seu ciclo de vida.

Monte a ferramenta no Vite

Para montar uma ferramenta do Devframe no Vite, use devtools.nodeMiddleware a partir de um hook configureServer, já que o Vite trabalha com middlewares no estilo Connect. Crie a instância dentro do hook para que ela possa compartilhar o servidor HTTP do 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' mantém o painel fora dos builds de produção. A opção server coloca o WebSocket de RPC no próprio servidor HTTP do Vite, de modo que nenhuma porta extra é aberta. O Rsbuild aceita nodeMiddleware da mesma forma, mas seus handlers nunca recebem requisições de upgrade, então ele precisa de ws: { sidecar: true } para o socket.

Monte a mesma ferramenta no Hono

O Hono lida nativamente com requisições Web Standard, então basta passar a requisição bruta para devtools.handler e anexar o WebSocket ao servidor Node. Este exemplo reaproveita o módulo devtools.ts criado anteriormente. Só o código de integração muda:

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

O catch-all app.all é uma rota comum, a mesma ideia de escopo usada ao escrever seu próprio middleware para Hono. c.req.raw é o Request Web Standard subjacente. Um fetch handler só enxerga requisições individuais, e um devframe em execução não se vincula a nenhuma porta, então attach() repassa os eventos de upgrade do servidor Node para o socket do devframe. Sem essa chamada, a UI carrega, mas o RPC em tempo real fica sem conexão.

Outros hosts e adapters empacotados

Todo host do Devframe segue uma única regra: hosts Web Standard montam handler, dev servers no estilo Connect montam nodeMiddleware, e hosts cujos route handlers nunca recebem requisições de upgrade usam um WebSocket side-car.

HostMontagemRota / arquivoWebSocket
VitenodeMiddleware via server.middlewares.useplugin com configureServerserver: server.httpServer
Honohandler(c.req.raw)app.all('/__my-tool/*', …)devtools.attach(serve(...))
Nitrohandler(event.req)routes/__my-tool/[...path].ts mais 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 }

O Next.js trata pastas que começam com underscore como privadas. Por isso, uma rota servida em /__my-tool/ fica em uma pasta chamada %5F_my-tool, que é decodificada de volta para __my-tool. Hosts que recarregam módulos durante o desenvolvimento, como Next.js, Nitro e SvelteKit, devem memoizar a instância em globalThis para que cada recarga não crie um novo devframe. A página do initiate mostra essa memoização tipada como ReturnType<typeof initDevframe>.

A mesma definição também pode ser empacotada sem um host. Todas estas opções estão listadas no índice de adapters:

  • CLI standalone: createCac(), de devframe/adapters/cac. Requer a peer dependency opcional cac.
  • Dev server: createDevServer(), de devframe/adapters/dev.
  • Build estático: createBuild(), de devframe/adapters/build, para relatórios em snapshot.
  • Servidor MCP: createMcpServer(), de devframe/adapters/mcp, para agentes de código. As funções RPC ficam privadas para os agentes, a menos que sejam explicitamente habilitadas.

Projetos de referência funcionais para vários hosts estão na pasta examples/ do repositório.

Mantenha o controle de autenticação ativado

O Devframe controla o acesso por padrão. Quando um navegador ainda não autorizado solicita acesso, o terminal exibe um código e um magic link. O Devframe nunca deriva a origem do magic link de um header Host bruto nem de headers encaminhados, então qualquer deploy que não esteja em loopback precisa definir origin explicitamente. A opção auth: false existe, mas só é adequada para um único usuário em localhost.

Quando vale a pena usar o Devframe e quando uma extensão é mais simples?

O Devframe compensa quando a ferramenta precisa de estado do lado do servidor e tem que rodar em mais de um host. Se a ferramenta só inspeciona a página em um único framework, uma extensão de navegador dá menos trabalho.

Escolha o Devframe quando:

  • a ferramenta lê estado do lado do Node, como o grafo de build, a configuração, o ambiente do processo ou o sistema de arquivos;
  • a biblioteca é usada em vários frameworks ou dev servers;
  • você quer distribuir a mesma ferramenta como CLI, relatório estático de CI ou servidor MCP.

Escolha uma extensão de navegador quando:

  • a ferramenta só inspeciona o que já está na página, como o DOM ou um store do lado do cliente;
  • ela é voltada para o runtime de um único framework;
  • não há dados do lado do servidor a serem buscados.

Conclusão

Com o Devframe, o suporte multiframework se resume a algumas linhas de código de montagem por host. Você escreve a ferramenta uma única vez como uma definição, initDevframe() a transforma em um handler padrão, e cada host conecta esse handler ao seu roteador e ao seu WebSocket. Como próximo passo, complete a definição com os metadados obrigatórios e uma entrada de navegador, conforme o guia de definição do Devframe. Depois, monte a ferramenta no host mais usado pelos seus usuários e adicione os demais com base na tabela.

Perguntas frequentes

Como expor uma função RPC do Devframe para um agente de código?

Adicione um campo agent com uma descrição em defineRpcFunction, por exemplo agent: { description: 'Returns a summary' }. O campo agent ativa a serialização JSON estrita, porque o MCP só aceita dados em formato JSON, e combiná-lo com jsonSerializable: false lança o erro DF0019. Sirva a definição com createMcpServer(devframe, { transport: 'stdio' }), de devframe/adapters/mcp, que exige a peer dependency opcional @devframes/agentic. Funções sem o campo agent continuam sendo apenas RPC interno.

Posso usar o Devframe sem o Vite ou em um projeto CommonJS?

Sim. O Devframe não depende do Vite, então roda em qualquer host Web Standard, como o Hono, ou como ferramenta standalone por meio de createDevServer ou createCac. Ele é distribuído apenas como ESM, então uma base de código CommonJS precisa carregá-lo com imports ESM ou com uma chamada import() dinâmica. Se você usar o adapter de CLI sem instalar a peer dependency opcional cac, o erro aparece no momento do import.

Quais tipos de função RPC o Devframe suporta além de static?

As funções RPC do Devframe usam um de quatro tipos: query, static, action e event. Apenas funções static e query podem ter um campo dump para resultados pré-computados. Adicionar dump a uma função action ou event lança um erro de validação. Os argumentos declarados em args são verificados com qualquer validador compatível com Standard Schema, como o Valibot, antes de o handler ser executado.

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.