Este guia orienta você na construção de um plugin nativo do zero. Plugins nativos são executados no mesmo processo que seu site Astro com acesso total ao runtime, incluindo páginas de administração React, componentes Portable Text e fragmentos de página.
Se você ainda não decidiu se deseja um plugin nativo em vez de um sandboxed, leia primeiro Escolhendo um formato de plugin. Nativo é o formato para plugins que precisam de páginas de administração React, componentes de renderização Portable Text ou fragmentos de página.
Duas peças, em um ou dois arquivos
Como plugins sandboxed, plugins nativos incluem duas peças:
- Uma factory de descriptor — retorna um
PluginDescriptorcomformat: "native"mais pontos de entrada relacionados ao admin. Importado porastro.config.mjsem tempo de build. - Uma função
createPlugin(options)— o lado do runtime. Retorna um resultadodefinePlugin({ id, version, capabilities, hooks, routes, admin }).
Diferentemente de plugins sandboxed, ambas as peças podem viver no mesmo arquivo porque não são executadas em ambientes diferentes — todo o plugin é executado in-process. A exportação "." do pacote aponta para um arquivo que exporta tanto a factory do descriptor quanto uma função createPlugin (ou default):
my-native-plugin/
├── src/
│ ├── index.ts # Descriptor factory + createPlugin
│ ├── admin.tsx # React admin components (optional)
│ └── astro/ # Astro components for PT block rendering (optional)
│ └── index.ts
├── package.json
└── tsconfig.json
Configurar o pacote
O seguinte package.json declara os pontos de entrada e as peer dependencies que um plugin nativo precisa:
{
"name": "@my-org/plugin-analytics",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./admin": {
"types": "./dist/admin.d.ts",
"import": "./dist/admin.js"
}
},
"files": ["dist"],
"peerDependencies": {
"emdash": "*",
"react": "^18.0.0"
}
}
Mantenha emdash e react como peer dependencies para que o site host forneça as versões reais e você não envie duplicatas.
Escrever o descriptor e o runtime
O seguinte src/index.ts define a factory do descriptor e o runtime createPlugin em um arquivo:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface AnalyticsOptions {
enabled?: boolean;
maxEvents?: number;
}
export function analyticsPlugin(options: AnalyticsOptions = {}): PluginDescriptor {
return {
id: "analytics",
version: "0.1.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
options,
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin(options: AnalyticsOptions = {}) {
const maxEvents = options.maxEvents ?? 100;
return definePlugin({
id: "analytics",
version: "0.1.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: {
events: { indexes: ["type", "createdAt"] },
},
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("Analytics plugin installed", { maxEvents });
},
"content:afterSave": async (event, ctx) => {
const enabled = await ctx.kv.get<boolean>("settings:enabled");
if (enabled === false) return;
await ctx.storage.events.put(`evt_${Date.now()}`, {
type: "content:save",
contentId: event.content.id,
createdAt: new Date().toISOString(),
});
},
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({
createdAt: { gte: today },
});
return { today: count };
},
},
},
});
}
export default createPlugin;
Detalhes-chave desta configuração:
format: "native"é obrigatório."native"também é o padrão, mas declarar explicitamente em cada descriptor torna o formato fácil de identificar.entrypointé a exportação principal do pacote. EmDash o importa em tempo de execução e chama a exportação padrão para construir o plugin resolvido.optionsfluem do descriptor paracreatePlugin. Tudo o que o usuário passa ao registrar o plugin (analyticsPlugin({ enabled: false })) é preservado no descriptor e encaminhado paracreatePlugin. Plugins sandboxed não têm essa superfície — eles leem configurações do KV em vez disso.id,versionecapabilitiesaparecem duas vezes. Uma vez no descriptor, uma vez emdefinePlugin(). Eles devem coincidir. A cópia do descriptor é o queastro.config.mjsvê em tempo de build; a cópia dedefinePlugin()é o que é executado em tempo de requisição.- Manipuladores de rota nativos recebem um único argumento —
(ctx: RouteContext)ondectx.input,ctx.requestectx.requestMetasão mesclados com as propriedades regulares doPluginContext. Isso é o oposto da forma de dois argumentos do formato padrão. Veja Rotas API para a superfície completa (todo o resto é idêntico).
Regras de ID de plugin
O campo id deve corresponder a /^[a-z][a-z0-9_-]*$/ — começar com uma letra minúscula, depois letras, dígitos, hífens ou underscores. O id é usado como um único segmento de caminho em URLs de rotas de plugin e como parte de identificadores SQL gerados para índices de armazenamento de plugin, então qualquer coisa fora desse padrão falha em tempo de execução. Os seguintes valores mostram quais IDs são aceitos:
// Valid
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// Invalid
"@my-org/plugin-forms"; // scoped form not allowed at runtime
"MyPlugin"; // no uppercase
"42-plugin"; // can't start with a digit
"my.plugin"; // no dots
Emparelhe um id sem escopo com um nome de pacote npm com escopo em entrypoint — o nome do pacote e o id do plugin são preocupações separadas.
Formato de versão
Use versionamento semântico. Os seguintes valores mostram quais strings de versão são aceitas:
version: "1.0.0"; // valid
version: "1.2.3-beta"; // valid (prerelease)
version: "1.0"; // invalid (missing patch)
Registrar o plugin
No astro.config.mjs do seu site, importe a factory do descriptor e passe-a para o array plugins: [] — plugins nativos sempre são executados in-process, nunca em sandboxed: []:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { analyticsPlugin } from "@my-org/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [
analyticsPlugin({ enabled: true, maxEvents: 500 }),
],
}),
],
});
UI de configurações
Plugins nativos podem usar admin.settingsSchema para um formulário de configurações gerado automaticamente, que é o caminho mais simples:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
maxItems: { type: "number", label: "Max items", min: 1, max: 1000, default: 100 },
},
},
Tipos de campo: string, number, boolean, select, secret, url, email. Cada um aceita label, description, default, mais extras específicos do tipo como min/max/options. As configurações são persistidas no mesmo armazenamento KV por plugin que os plugins sandboxed usam — leia-as com ctx.kv.get<T>("settings:<key>") de qualquer lugar.
Para uma UI de configurações mais rica do que settingsSchema fornece, envie páginas React personalizadas — veja Páginas de administração e widgets React.
Exemplo completo — plugin de log de auditoria
O seguinte plugin registra toda criação, atualização e exclusão de conteúdo em armazenamento indexado e expõe uma rota de atividade recente:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
interface AuditEntry {
timestamp: string;
action: "create" | "update" | "delete";
collection: string;
resourceId: string;
userId?: string;
}
export function auditLogPlugin(): PluginDescriptor {
return {
id: "audit-log",
version: "0.1.0",
format: "native",
entrypoint: "@emdash-cms/plugin-audit-log",
};
}
export function createPlugin() {
return definePlugin({
id: "audit-log",
version: "0.1.0",
storage: {
entries: {
indexes: [
"timestamp",
"action",
"collection",
["collection", "timestamp"],
["action", "timestamp"],
],
},
},
admin: {
settingsSchema: {
retentionDays: {
type: "number",
label: "Retention (days)",
description: "Days to keep entries. 0 = forever.",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "Audit History", icon: "history" }],
widgets: [{ id: "recent-activity", title: "Recent Activity", size: "half" }],
},
hooks: {
"content:afterSave": {
priority: 200,
handler: async (event, ctx) => {
const entry: AuditEntry = {
timestamp: new Date().toISOString(),
action: event.isNew ? "create" : "update",
collection: event.collection,
resourceId: event.content.id as string,
};
await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, entry);
},
},
"content:afterDelete": {
priority: 200,
handler: async (event, ctx) => {
await ctx.storage.entries.put(`${Date.now()}-${event.id}`, {
timestamp: new Date().toISOString(),
action: "delete",
collection: event.collection,
resourceId: event.id,
});
},
},
},
routes: {
recent: {
handler: async (ctx) => {
const result = await ctx.storage.entries.query({
orderBy: { timestamp: "desc" },
limit: 10,
});
return {
entries: result.items.map((item) => ({
id: item.id,
...(item.data as AuditEntry),
})),
};
},
},
},
});
}
export default createPlugin;
Teste
Teste um plugin nativo criando um site Astro mínimo com o plugin registrado:
- Crie um site de teste com EmDash instalado.
- Registre seu plugin em
astro.config.mjs, importando-o diretamente do seu caminho de origem local. - Execute o servidor de desenvolvimento e acione hooks criando, atualizando ou excluindo conteúdo.
- Verifique o console para a saída de
ctx.loge verifique o armazenamento via rotas API.
Para testes unitários, simule a interface PluginContext e chame os manipuladores de hook diretamente.
O que vem a seguir
- Páginas de administração e widgets React — envie UI React personalizada para o painel de administração.
- Componentes de renderização Portable Text — forneça componentes Astro que renderizam tipos de bloco definidos por plugin.
- Fragmentos de página — injete scripts, folhas de estilo ou HTML em páginas públicas.
- Distribuição de plugins nativos — empacotamento npm e versionamento.