Arquitetura de Micro-Frontend
O CPanel (shell) hospeda as UIs dos plugins como micro-frontends isolados. Cada
plugin com um entrypoint HTML torna-se um worker independente embutível via
<z-frame> (@zomme/frame). A comunicação acontece sobre um MessageChannel
bidirecional em cima de postMessage.
Para saber como os plugins declaram sua UI, veja Sistema de Plugins. Para o worker pool que serve cada iframe, veja Worker Pool.
Objetivos
Seção intitulada “Objetivos”| Objetivo | Como |
|---|---|
| Modularidade | Cada plugin entrega sua própria UI como um worker |
| Independência | Build/deploy isolado por plugin |
| Agnóstico de framework | React, Solid, Qwik, Vue — qualquer um deles |
| Isolamento de segurança | Iframes em sandbox, sem acesso ao DOM do shell |
| Comunicação tipada | MessageChannel + serialização automática (props, eventos, RPC) |
Topologia
Seção intitulada “Topologia”Buntime Runtime (port 8000)├── Shell (CPanel)│ ├── Layout, navigation│ └── <z-frame> elements ──┐└── Workers │ ├── Plugin UI A ◄───────┤ MessageChannel ├── Metrics ◄───────┤ (props sync, RPC, events) ├── Logs ◄───────┤ └── ... ◄───────┘Pacotes
Seção intitulada “Pacotes”| Pacote | Papel |
|---|---|
@zomme/frame | Web component <z-frame> (lado do shell) e frameSDK (lado do iframe) |
@zomme/frame-react | Bindings React (useFrameSDK, useRouteSync) |
<z-frame> — Lado do Shell
Seção intitulada “<z-frame> — Lado do Shell”Web component que carrega um iframe e gerencia o MessageChannel.
<z-frame name="metrics" base="/metrics" src="http://localhost:8000/metrics" pathname="/files" theme="dark"></z-frame>Atributos
Seção intitulada “Atributos”| Atributo | Tipo | Descrição |
|---|---|---|
name | string | Identificador (obrigatório) |
src | string | URL da aplicação no iframe (obrigatório) |
base | string | Caminho base para roteamento (padrão: /<name>) |
pathname | string | Caminho inicial (padrão: /) |
sandbox | string | Permissões do iframe |
API JavaScript
Seção intitulada “API JavaScript”const frame = document.querySelector("z-frame");
// Dynamic props — automatically synced to the iframeframe.theme = "dark";frame.user = currentUser;frame.apiUrl = "https://api.example.com";
// Emit events to the iframeframe.emit("route-change", { path: "/settings" });
// RPC: call a function registered by the iframeconst stats = await frame.getStats();
// Listen to events coming from the iframeframe.addEventListener("ready", () => {});frame.addEventListener("navigate", (e) => router.push(e.detail.path));frameSDK — Lado do Iframe
Seção intitulada “frameSDK — Lado do Iframe”import { frameSDK } from "@zomme/frame/sdk";
await frameSDK.initialize(); // required before use
// Access props passed by the shellconsole.log(frameSDK.props.base); // "/metrics"console.log(frameSDK.props.theme); // "dark"
// Call functions passed by the shell (props that are functions)await frameSDK.props.onSuccess({ status: "ok" });
// Emit events to the shellframeSDK.emit("navigate", { path: "/settings" });
// Listen to events from the shellframeSDK.on("route-change", ({ path }) => router.navigate(path));
// Register functions for the shell to callframeSDK.register({ refreshData: async () => loadData(), getStats: () => ({ count: 42 }),});
// Watch for changes on specific propsframeSDK.watch(["theme"], (changes) => { if ("theme" in changes) { const [next, prev] = changes.theme; applyTheme(next); }});Bindings React
Seção intitulada “Bindings React”import { useFrameSDK, useRouteSync } from "@zomme/frame-react";
function App() { const { props, isReady } = useFrameSDK();
// Sync route with shell useRouteSync({ onRouteChange: (path) => router.navigate(path), getCurrentPath: () => router.currentPath, });
if (!isReady) return <Loading />;
return <h1>Theme: {props.theme}</h1>;}Plugin com UI — Estrutura
Seção intitulada “Plugin com UI — Estrutura”plugins/my-plugin/├── manifest.yaml # entrypoint: dist/client/index.html├── plugin.ts # Middleware in the main process├── server/api.ts # API (for serverless, goes into index.ts)├── client/│ ├── index.tsx # React entry│ ├── index.html # Shell HTML│ ├── utils/use-frame-sdk.ts # Local hook│ └── components/└── dist/ ├── plugin.js └── client/index.htmlManifesto
Seção intitulada “Manifesto”name: "@buntime/my-plugin"base: "/my-plugin"entrypoint: dist/client/index.html # HTML → automatic SPA modemenus: - title: My Plugin icon: lucide:cloud-upload path: /my-pluginNão existe mais um campo fragment no manifesto. Plugins com um entrypoint
HTML ficam automaticamente disponíveis como micro-frontends.
Ponto de entrada do client
Seção intitulada “Ponto de entrada do client”import { createRoot } from "react-dom/client";import { frameSDK } from "@zomme/frame/sdk";
await frameSDK.initialize();frameSDK.register({ refresh: () => window.location.reload() });createRoot(document.getElementById("root")!).render(<MyPluginPage />);Integração com o Shell (CPanel)
Seção intitulada “Integração com o Shell (CPanel)”O CPanel envolve <z-frame> em um componente React. Escutar o evento navigate
vindo do iframe e propagá-lo via window.history.pushState mantém a URL do shell
em sincronia com a navegação interna do plugin. As props
(base, pathname, theme) são passadas como atributos/propriedades em
<z-frame> e automaticamente sincronizadas com o iframe via PROPS_UPDATE.
Comunicação — Protocolo
Seção intitulada “Comunicação — Protocolo”Fluxo de Inicialização
Seção intitulada “Fluxo de Inicialização”Shell (z-frame) Iframe frameSDK │ creates iframe (src) │ │ │──────────────────────────▶ │ │ │ │ load │ │ │ ──── frameSDK.initialize() ─▶ │ postMessage(INIT, props, │ │ │ [port2]) │ │ │──────────────────────────▶ │ │ │ │ receives port2, props │ │ │ ◀─── port.postMessage(READY) ── │ emit('ready') │ │Tipos de Mensagem
Seção intitulada “Tipos de Mensagem”| Tipo | Direção | Propósito |
|---|---|---|
INIT | Shell → Frame | Props iniciais + MessagePort |
READY | Frame → Shell | Frame inicializado |
PROPS_UPDATE | Shell → Frame | Atualização de props |
EVENT | Shell → Frame | Evento personalizado |
CUSTOM_EVENT | Frame → Shell | Evento personalizado |
FUNCTION_CALL | Bidirecional | Chamada RPC |
FUNCTION_RESPONSE | Bidirecional | Valor de retorno RPC |
Funções como Props
Seção intitulada “Funções como Props”As funções são serializadas automaticamente — proxy virtual via RPC:
// Shell: passes function as propframe.onSave = async (data) => { await api.save(data); return { success: true };};
// Frame: calls transparentlyconst result = await frameSDK.props.onSave({ id: 123 });console.log(result.success); // trueFunções Registradas
Seção intitulada “Funções Registradas”// Frame: registersframeSDK.register("getStats", () => ({ users: 42 }));
// Shell: callsconst stats = await frame.getStats();Injeção do Caminho Base
Seção intitulada “Injeção do Caminho Base”O runtime injeta <base href="/plugin-name/"> no HTML servido ao iframe. Isso
permite que o roteador SPA do plugin funcione como se estivesse na raiz, ao mesmo
tempo em que resolve corretamente caminhos relativos:
function getApiBase(): string { // Before (piercing/Shadow DOM): complex getRootNode logic // Now (frame): simple const base = document.querySelector("base"); return base?.getAttribute("href")?.replace(/\/$/, "") || "/plugin";
// Or via SDK: return frameSDK.props.base;}A injeção é feita em wrapper.ts quando ele detecta uma resposta HTML mais o
cabeçalho X-Base. O conteúdo passa por escape de HTML para prevenir XSS. Veja
o Runtime para detalhes sobre o mecanismo.
Benefícios e Limitações
Seção intitulada “Benefícios e Limitações”Benefícios
Seção intitulada “Benefícios”| Benefício | Como acontece |
|---|---|
| Isolamento de segurança | Iframe em sandbox — sem acesso ao DOM do shell |
| Deploy independente | Plugin atualizado sem reconstruir o runtime |
| Liberdade tecnológica | Cada plugin escolhe seu framework |
| Comunicação tipada | TypeScript para props/eventos via @zomme/frame |
| Lazy loading | Frames carregam sob demanda |
| Resiliência | Um erro em um frame não afeta o shell |
Limitações
Seção intitulada “Limitações”- Cada frame paga o overhead de um processo + bundle.
- O estado global compartilhado requer o shell como mediador.
- O DevTools é mais complexo — abra o frame em “Open frame in new tab” para depuração isolada.
Migrando de @buntime/piercing
Seção intitulada “Migrando de @buntime/piercing”O sistema antigo usava Shadow DOM com piercing. Para migrar:
- Remova a seção
fragmentdomanifest.yaml. - Substitua os imports de
@buntime/piercingpor@zomme/frame(ou@zomme/frame-react). - Inicialize o SDK:
await frameSDK.initialize()no entry do client. - Substitua o acesso ao Shadow DOM por
useFrameSDK()(ouframeSDKdiretamente). - Simplifique
getApiBase()para usar<base>ouframeSDK.props.base.
Documentação Relacionada
Seção intitulada “Documentação Relacionada”- O Runtime — injeção de
<base href>, cabeçalhosX-Base/X-Not-Found. - Sistema de Plugins — manifesto com
entrypoint,menus,injectBase. - Worker Pool — wrapper que serve o HTML do iframe.
- Guia de SPA App Shell — construindo um SPA de plugin contra o shell.