Sistema de Plugins
O mecanismo de extensibilidade do runtime. Plugins são unidades isoladas que podem interceptar requisições, registrar rotas, fornecer serviços a outros plugins e expor UI no shell. O sistema suporta hot reload, dependências declarativas e dois modos de execução (persistente vs serverless).
Para o pipeline de requisições que envolve plugins, veja Runtime. Para o pool que executa plugins serverless, veja Worker Pool.
Arquitetura
Seção intitulada “Arquitetura”pluginDirs (RUNTIME_PLUGIN_DIRS) │ ▼PluginLoader — scan + parse manifest.yaml + topological sort + lazy import │ ▼PluginRegistry — stores plugins, runs hooks, resolves routes, shares services │ ▼Hooks: onInit · onRequest · onResponse · onShutdown onServerStart · onWorkerSpawn · onWorkerTerminate| Componente | Papel |
|---|---|
PluginLoader | Descoberta automática em pluginDirs, parsing do manifest.yaml, ordenação topológica, lazy import |
PluginRegistry | Armazena plugins, executa hooks, gerencia o compartilhamento de serviços, resolve rotas |
Estrutura de um plugin
Seção intitulada “Estrutura de um plugin”plugins/plugin-example/├── manifest.yaml # Metadata + config├── plugin.ts # Middleware (persistent mode, main process)├── index.ts # Worker entrypoint (serverless mode, worker pool)├── server/api.ts # Shared API code├── client/ # SPA (React/Solid/Vue/etc.)└── dist/ # Build output ├── plugin.js ├── index.js └── client/index.htmlModos de API — persistente vs serverless
Seção intitulada “Modos de API — persistente vs serverless”Uma decisão central. Escolha um — não duplique a API entre plugin.ts
e index.ts.
| Modo | Quando usar | plugin.ts tem routes? | index.ts tem routes? | entrypoint |
|---|---|---|---|---|
| Persistente | Conexões de DB, WebSocket/SSE, cache em memória, cron, estado compartilhado | Sim | Não (apenas SPA) | dist/client/index.html |
| Serverless | CRUD stateless, operações de arquivo, isolamento, escala horizontal | Não | Sim | dist/index.js |
Roda no processo principal. A API fica em plugin.ts; index.ts apenas
serve a SPA.
// plugin.ts — runs in the main process (persistent)export default function databasePlugin(config): PluginImpl { let db: DatabasePool; return { routes: api, // API here (a Hono app) onInit(ctx) { db = new DatabasePool(config.url); // persistent connection }, };}
// index.ts — SPA onlyexport default { fetch: createStaticHandler(clientDir) };Plugins persistentes atuais: gateway, keyval, logs, metrics,
proxy, turso, vhosts.
Roda no worker pool, por requisição. A API fica em index.ts;
plugin.ts não tem rotas.
// plugin.ts — no routesexport default (config): PluginImpl => ({ onInit(ctx) { /* optional */ },});
// index.ts — API in the worker poolexport default { routes: { "/api/*": api.fetch }, fetch: createStaticHandler(clientDir),};Modos de entrypoint
Seção intitulada “Modos de entrypoint”| Extensão | Modo | Comportamento |
|---|---|---|
.html | SPA | Apenas serveStatic; index.ts NÃO é executado |
.js / .ts | Service | Importa o módulo; espera export default { fetch?, routes? } |
Schema do manifest
Seção intitulada “Schema do manifest”name: "@buntime/plugin-example" # Unique identifierbase: "/example" # /[a-zA-Z0-9_-]+; omit for hook-only pluginsenabled: true # default: true
# Worker / plugin entrypointsentrypoint: dist/index.js # service mode (or .html for SPA)pluginEntry: dist/plugin.js # middleware in the main process
# Dependencies (topological sort)dependencies: - "@buntime/plugin-turso" # required — fails if absentoptionalDependencies: - "@buntime/plugin-keyval" # ignored if absent
# Auth bypass via onRequestpublicRoutes: ALL: ["/health"] GET: ["/api/public/**"] POST: ["/api/webhook"]
# Shell menus (cpanel)menus: - { icon: lucide:box, path: /example, title: Example }
# Env vars for workersenv: MY_VAR: "value"
# Config schema for Helm/Rancher UIconfig: apiKey: type: password label: API Key env: EXAMPLE_API_KEY # maps to ConfigMapCampos principais
Seção intitulada “Campos principais”| Campo | Tipo | Descrição |
|---|---|---|
name | string | Identificador único, formato @scope/plugin-name |
base | string | Caminho base para as routes do plugin |
enabled | boolean | false para ignorar no carregamento |
entrypoint | string | Entrypoint do worker (HTML para SPA, JS para service) |
pluginEntry | string | Middleware do processo principal |
dependencies | string[] | Plugins obrigatórios (erro se ausentes) |
optionalDependencies | string[] | Plugins opcionais (ignorados se ausentes) |
publicRoutes | array | object | Ignoram (bypass) os hooks onRequest |
menus | MenuItem[] | Itens de menu para o shell |
injectBase | boolean | Controla a injeção de <base href> |
visibility | enum | public | protected | internal |
env | record | Vars para os workers do plugin |
config | object | Schema de config para geração do Helm |
Plugins de infraestrutura apenas com hooks que não servem rotas devem omitir
base por completo — não defina base: "", que mais tarde pode ser
confundido com o base de um plugin-app na raiz durante a resolução de rotas.
Descoberta automática
Seção intitulada “Descoberta automática”PluginLoader varre pluginDirs (estilo PATH, separados por :). Estruturas
suportadas:
1. Direct: {pluginDir}/plugin.ts + manifest.yaml2. Subdirectory: {pluginDir}/{name}/plugin.ts + manifest.yaml3. Scoped: {pluginDir}/@scope/{name}/plugin.ts + manifest.yamlPrioridade do arquivo de entrada: manifest.pluginEntry → plugin.{ts,js} →
index.{ts,js}. Padrão para RUNTIME_PLUGIN_DIRS: /data/.plugins:/data/plugins
(internos primeiro, externos depois).
Ordenação topológica — algoritmo de Kahn
Seção intitulada “Ordenação topológica — algoritmo de Kahn”Os plugins são ordenados por dependência antes do carregamento, usando o algoritmo de Kahn (O(V + E)):
- Calcula o grau de entrada (número de dependências) de cada plugin.
- Enfileira os plugins com grau de entrada zero.
- Para cada plugin processado, decrementa o grau de entrada dos seus dependentes; enfileira os que chegarem a zero.
- Qualquer plugin que reste sem processar → um ciclo de dependências, que é um erro fatal.
Exemplo: com keyval dependendo de turso, a ordem resultante é sempre
turso → keyval, independentemente da ordem no sistema de arquivos.
Hooks de ciclo de vida
Seção intitulada “Hooks de ciclo de vida”interface PluginImpl { routes?: Hono; middleware?: MiddlewareHandler; server?: { routes?; fetch? }; websocket?: { open?; message?; close? }; provides?: () => unknown | Promise<unknown>;
onInit?(ctx: PluginContext): void | Promise<void>; onShutdown?(): void | Promise<void>; onServerStart?(server): void; onRequest?(req, app?): Request | Response | undefined; onResponse?(res, app): Response; onWorkerSpawn?(worker, app): void; onWorkerTerminate?(worker, app): void;}| Hook | Quando | Retorno | Notas |
|---|---|---|---|
onInit(ctx) | No carregamento do plugin | void/Promise | timeout de 30s — falha fatal se excedido |
onShutdown() | No SIGINT | void/Promise | Ordem reversa (LIFO) |
onServerStart(server) | Após Bun.serve() | void | Acesso à instância para upgrade de WS |
onRequest(req, app?) | Antes de cada handler | Request (modificado) | Response (curto-circuito) | undefined | Ordem topológica |
onResponse(res, app) | Após a geração da resposta | Response | Ordem topológica |
onWorkerSpawn(worker, app) | Worker criado | void | — |
onWorkerTerminate(worker, app) | Worker terminado | void | — |
onInit — o contexto
Seção intitulada “onInit — o contexto”interface PluginContext { config: Record<string, unknown>; // from manifest.yaml globalConfig: GlobalPluginConfig; // workerDirs, poolSize, pluginDirs logger: PluginLogger; // scoped logger auth?: PluginAuthContext; // API key store + master key pool?: WorkerPool; // worker pool, if needed getPlugin<T>(name: string): T | undefined; // another plugin's exports runtime: { api: string; version: string }; // same as /.well-known/buntime}O padrão canônico de leitura de config lê Bun.env primeiro (ConfigMap/Helm),
depois a config do manifest e, então, um valor padrão:
const apiKey = Bun.env.MY_API_KEY ?? pluginConfig.apiKey ?? "default";onRequest — curto-circuito
Seção intitulada “onRequest — curto-circuito”onRequest(req, app) { if (rateLimitExceeded) { return new Response("Too Many Requests", { status: 429 }); // short-circuit } return undefined; // continue the pipeline}| Retorno | Comportamento |
|---|---|
undefined | Continua com a requisição original |
Request | Continua com a requisição modificada |
Response | Curto-circuito — o runtime retorna imediatamente |
Os plugins rodam em ordem topológica, ex. Metrics → Proxy → Gateway → Worker.
Compartilhamento de serviços — provides / getPlugin
Seção intitulada “Compartilhamento de serviços — provides / getPlugin”Plugins compartilham funcionalidade sem importar uns aos outros. Um provedor
retorna sua superfície pública em provides(); um consumidor a alcança através
de ctx.getPlugin().
// Provider — plugin-turso/plugin.tsexport default (config): PluginImpl => { const service = new TursoService(config); return { provides: () => service, // exposed to other plugins onInit(ctx) { /* ... */ }, };};
// Consumer — must declare the dependency in its manifestexport default (config): PluginImpl => ({ onInit(ctx) { const turso = ctx.getPlugin<TursoService>("@buntime/plugin-turso"); if (!turso) throw new Error("Requires @buntime/plugin-turso"); },});Regras:
provides()pode ser síncrono ou assíncrono; o valor de retorno é o quegetPluginentrega.- Os exports ficam disponíveis após o
onInitdo provedor — declaredependenciespara que o loader os ordene corretamente.
Hot reload
Seção intitulada “Hot reload”# Upload a tarball/zip (extracts to pluginDirs; does NOT load yet)POST /api/plugins/upload
# Re-scan + reload all (loads newly uploaded plugins, no process restart)POST /api/plugins/reload
# Enable / disable a single plugin at runtime (no restart)POST /api/plugins/<url-encoded-name>/enablePOST /api/plugins/<url-encoded-name>/disablereload limpa o registry, re-varre os diretórios, ordena topologicamente,
re-executa onInit e atualiza a tabela de rotas nativa do servidor HTTP em
execução.
O runtime serve três tipos de superfície HTTP de plugin, cada uma alcançando o servidor em execução de forma diferente:
| Superfície | Declarada como | Como despacha | Faz hot-reload? |
|---|---|---|---|
| Rotas Hono | PluginImpl.routes | app.fetch consulta o registry a cada requisição | Sim — inerentemente dinâmica |
| Handler de fetch | PluginImpl.server.fetch | app.fetch itera o registry a cada requisição | Sim — inerentemente dinâmica |
| Rotas nativas | PluginImpl.server.routes | O Bun faz o match destas antes de app.fetch; a tabela é fixada no momento do Bun.serve() | Sim — reload chama server.reload({ routes }) |
enable/disable invertem a flag enabled do manifest do plugin no disco (uma
edição cirúrgica de linha que preserva comentários), e então re-varrem e
atualizam as rotas. Como o manifest é a fonte da verdade para o estado de
habilitado, a mudança sobrevive a reinícios. Os nomes são URL-encoded, então
nomes com escopo (scoped) funcionam:
POST /api/plugins/%40acme%2Fplugin-x/disable.
Rotas públicas
Seção intitulada “Rotas públicas”Ignoram (bypass) os hooks onRequest (auth). Dois formatos:
# Array — all methodspublicRoutes: - "/health" - "/api/public/**"
# Object — per methodpublicRoutes: ALL: ["/health"] GET: ["/api/users/**"] POST: ["/api/webhook"]Curingas: * (segmento único), ** (múltiplos segmentos). As rotas públicas de
plugin são absolutas; as rotas públicas de worker são relativas e tornadas
absolutas pelo runtime (/api/health → /todos-kv/api/health).
Plugins com UI
Seção intitulada “Plugins com UI”Plugins com um entrypoint HTML são automaticamente expostos como
micro-frontends no shell via <z-frame>:
name: "@buntime/plugin-keyval"base: "/keyval"entrypoint: dist/client/index.htmlpluginEntry: dist/plugin.jsmenus: - { title: KeyVal, icon: lucide:database, path: /keyval }Detalhes em Arquitetura de Micro-Frontend.
Boas práticas
Seção intitulada “Boas práticas”- Modo único — não duplique a API entre
plugin.tseindex.ts. Bun.env.X ?? config.x— leia com um fallback, nunca escreva.:para múltiplos valores — estilo PATH, nunca vírgulas.- Declare as dependências — obrigatórias em
dependencies, opcionais emoptionalDependencies. - Init rápido — há um timeout de 30s em
onInit. Faça lazy-load de conexões custosas. onShutdowngracioso — esvazie caches, feche o DB, limpe os timers.- Hooks leves —
onRequest/onResponserodam a cada requisição.
Relacionado
Seção intitulada “Relacionado”- Runtime — pipeline de requisições, ordem de resolução.
- Worker Pool — execução de plugins serverless.
- Escrevendo um plugin — um guia passo a passo.
- Visão geral de plugins — os core plugins inclusos.