12k
All articles

Создание кросс-фреймворковых инструментов разработчика с Devframe

Создавайте кроссплатформенные инструменты разработчика с Devframe: определите инструмент один раз и подключите стандартный обработчик и WebSocket в Vite, Hono, Next.js и других средах.

OpenReplay Team
OpenReplay Team
Создание кросс-фреймворковых инструментов разработчика с Devframe

Devframe — фреймворк-независимая основа для инструментов разработчика, распространяемая по лицензии MIT. Инструмент описывается один раз с помощью defineDevframe(), а initDevframe() превращает это описание в обработчик запросов на основе Web Standards, который может подключить любой совместимый хост.

Если вы сопровождаете библиотеку, вам наверняка хотелось иметь для неё панель-инспектор, что-то вроде React Developer Tools, но для внутреннего устройства вашей библиотеки. И тут возникает матрица поддержки: плагин для Vite, маршрут для Next.js, middleware для Hono, и для каждого из них нужен свой связующий серверный код.

В этой статье мы создадим небольшой инструмент на Devframe, превратим его в обработчик, подключим одно и то же описание в Vite и в Hono, а затем рассмотрим остальные хосты и готовые адаптеры. В завершение разберём случаи, когда проще обойтись браузерным расширением.

Ключевые выводы

  • defineDevframe() из devframe описывает инструмент один раз. initDevframe(def, { base }) из devframe/initiate превращает описание в работающий экземпляр, который предоставляет handler и nodeMiddleware.
  • Hono, Nitro, Next.js и SvelteKit подключают devtools.handler, который принимает Request и возвращает Response по стандарту Web Standards. Vite и Rsbuild подключают devtools.nodeMiddleware.
  • Работающий экземпляр devframe не открывает собственный порт. Где будет находиться RPC-соединение по WebSocket, решает хост: на общем сервере, через attach() или через ws: { sidecar: true }.
  • По умолчанию Devframe ограничивает доступ. auth: false допустим только для одного пользователя на localhost.
  • Next.js и SvelteKit обслуживают /__my-tool/ из папки маршрута с именем %5F_my-tool, и обоим требуется отдельный side-car WebSocket.

Какую проблему решает Devframe?

Devframe решает проблему «один плагин на каждый фреймворк»: без общего слоя каждому devtool-инструменту приходится поставлять отдельную интеграцию для каждого фреймворка-хоста. Devframe устраняет это дублирование, помещая между инструментом и его окружением единый стандартный обработчик. В анонсе версии 1.0 обработчик рассматривается как граница инструмента. UI, параметры подключения, live RPC, аутентификация и опциональная MCP-точка доступа находятся за ним, под единым префиксом пути.

Поскольку обработчик работает только с Request и Response, инструмент больше не зависит от API какого-либо конкретного dev-сервера. В релизе v1.0 публичный интерфейс был стабилизирован, а исходный код доступен в репозитории devframes/devframe под лицензией MIT.

Как описать инструмент 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 экспортируйте listEnvNames как RPC-функцию, обработчик которой возвращает Object.keys(process.env).sort(). Структура обёртки и её параметры описаны в руководстве по описанию Devframe. Там же перечислены поля, необходимые для полного описания: обязательны id, name, version, packageName, homepage, description и setup, кроме того, следует передать importMetaUrl: import.meta.url. В руководстве также поясняется, что контекст с областью видимости (scoped context) добавляет к идентификаторам RPC пространство имён в виде id экземпляра devframe.

Возврат только имён — осознанное решение с точки зрения раскрытия данных. Панель защищена механизмом аутентификации, но функция, которая никогда не читает значения, не сможет их и раскрыть, кто бы в итоге ни открыл панель.

Превращаем описание в обработчик

initDevframe() преобразует описание в работающий экземпляр, чей handler обслуживает весь инструмент под одним базовым путём. Передайте описание и base:

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

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

Как поясняется в документации по стандартному обработчику, одна базовая точка монтирования охватывает весь инструмент: обработчик отдаёт SPA, файл обнаружения __connection.json, RPC-сокет, механизм аутентификации и MCP-маршрут. Параметр base обязателен. И handler, и nodeMiddleware сами дожидаются готовности экземпляра, поэтому их можно подключать сразу. Экземпляр также предоставляет attach, handleUpgrade и close() для управления сокетом и жизненным циклом.

Подключение в Vite

Чтобы подключить инструмент Devframe в Vite, используйте devtools.nodeMiddleware в хуке configureServer, поскольку Vite работает с middleware в стиле Connect. Создайте экземпляр внутри хука, чтобы он мог использовать HTTP-сервер 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' не допускает попадания панели в production-сборки. Параметр server размещает RPC WebSocket на собственном HTTP-сервере Vite, поэтому дополнительный порт не открывается. Rsbuild принимает nodeMiddleware точно так же, но его обработчики никогда не получают upgrade-запросы, поэтому для сокета ему нужен ws: { sidecar: true }.

Подключение того же инструмента в Hono

Hono нативно обрабатывает запросы по Web Standards, поэтому достаточно передать исходный запрос в 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 — это обычный маршрут, построенный на той же идее ограничения области, что и при написании собственного middleware для Hono. c.req.raw — это исходный объект Request по Web Standards. Fetch-обработчик видит только отдельные запросы, а работающий экземпляр devframe не привязывается ни к какому порту, поэтому attach() передаёт события upgrade Node-сервера в сокет devframe. Без этого вызова UI загрузится, но live RPC останется без соединения.

Другие хосты и готовые адаптеры

Все хосты Devframe подчиняются одному правилу: хосты на Web Standards подключают handler, dev-серверы в стиле Connect подключают nodeMiddleware, а хосты, чьи обработчики маршрутов никогда не получают upgrade-запросы, используют side-car WebSocket.

ХостПодключениеМаршрут / файлWebSocket
VitenodeMiddleware через server.middlewares.useплагин с configureServerserver: 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, следует мемоизировать экземпляр в globalThis, чтобы при каждой перезагрузке не создавался новый devframe. На странице initiate показан пример такой мемоизации с типом ReturnType<typeof initDevframe>.

То же описание можно упаковать и без хоста. Все эти варианты перечислены в индексе адаптеров:

  • Автономный CLI: createCac() из devframe/adapters/cac. Требуется опциональная peer-зависимость cac.
  • Dev-сервер: createDevServer() из devframe/adapters/dev.
  • Статическая сборка: createBuild() из devframe/adapters/build — для отчётов-снимков.
  • MCP-сервер: createMcpServer() из devframe/adapters/mcp — для ИИ-агентов, пишущих код. RPC-функции недоступны агентам, если явно не разрешено иное.

Рабочие эталонные проекты для нескольких хостов находятся в папке examples/ репозитория.

Не отключайте механизм аутентификации

Devframe ограничивает доступ по умолчанию. Когда неавторизованный браузер запрашивает доступ, в терминале выводятся код и magic link. Devframe никогда не определяет origin для magic link по «сырому» заголовку Host или заголовкам проксирования (forwarded headers), поэтому в любом развёртывании, не работающем на loopback-интерфейсе, origin нужно задавать явно. Параметр auth: false существует, но допустим только для одного пользователя на localhost.

Когда стоит выбрать Devframe, а когда проще сделать расширение?

Devframe окупается, когда инструменту нужно состояние на стороне сервера и он должен работать более чем в одном хосте. Если инструмент лишь инспектирует страницу в одном фреймворке, браузерное расширение получится компактнее.

Выбирайте Devframe, если:

  • инструмент читает состояние на стороне Node: граф сборки, конфигурацию, окружение процесса или файловую систему;
  • библиотека используется в нескольких фреймворках или dev-серверах;
  • вы хотите поставлять один и тот же инструмент как CLI, статический отчёт для CI или MCP-сервер.

Выбирайте браузерное расширение, если:

  • инструмент инспектирует только то, что уже есть на странице, например DOM или клиентское хранилище состояния;
  • он ориентирован на рантайм одного фреймворка;
  • данных на стороне сервера, которые нужно получать, нет.

Заключение

С Devframe поддержка нескольких фреймворков сводится к нескольким строкам кода подключения для каждого хоста. Вы один раз пишете инструмент в виде описания, initDevframe() превращает его в стандартный обработчик, а каждый хост подключает этот обработчик к своему маршрутизатору и своему WebSocket. Дальше дополните описание обязательными метаданными и браузерной точкой входа согласно руководству по описанию Devframe, затем подключите его в хосте, которым чаще всего пользуются ваши пользователи, и добавьте остальные по таблице.

Часто задаваемые вопросы

Как сделать RPC-функцию Devframe доступной для ИИ-агента?

Добавьте в defineRpcFunction поле agent с описанием, например agent: { description: 'Returns a summary' }. Поле agent включает строгую JSON-сериализацию, поскольку MCP принимает только данные в формате JSON, а его сочетание с jsonSerializable: false приводит к ошибке DF0019. Обслуживайте описание с помощью createMcpServer(devframe, { transport: 'stdio' }) из devframe/adapters/mcp — для этого нужна опциональная peer-зависимость @devframes/agentic. Функции без поля agent остаются только внутренними RPC-функциями.

Можно ли использовать Devframe без Vite или в проекте на CommonJS?

Да. Devframe не зависит от Vite, поэтому работает в любом хосте на Web Standards, например в Hono, или как автономный инструмент через createDevServer или createCac. Пакет поставляется только в формате ESM, поэтому в кодовой базе на CommonJS его нужно загружать через ESM-импорты или динамический вызов import(). Если вы используете CLI-адаптер, не установив его опциональную peer-зависимость cac, ошибка возникнет в момент импорта.

Какие типы RPC-функций, помимо static, поддерживает Devframe?

RPC-функции Devframe относятся к одному из четырёх типов: query, static, action и event. Поле dump с предварительно вычисленными результатами могут содержать только функции типов static и query. Добавление dump к функции типа action или event приводит к ошибке валидации. Аргументы, объявленные в args, перед запуском обработчика проверяются любым валидатором, совместимым со Standard Schema, например Valibot.

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.