Seu primeiro plugin nativo

Nesta página

Este guia acompanha você na construção de um plugin nativo do zero. Plugins nativos executam no mesmo processo que seu site Astro com acesso total ao runtime, incluindo páginas admin React, componentes Portable Text e fragmentos de página.

Se você ainda não decidiu se quer um plugin nativo ao invés de um sandboxed, leia primeiro Escolhendo um formato de plugin.

Duas peças, em um ou dois arquivos

  1. Uma fábrica de descritor — retorna um PluginDescriptor com format: "native".
  2. Uma função createPlugin(options) — o lado do runtime.
my-native-plugin/
├── src/
│   ├── index.ts          # Fábrica de descritor + createPlugin
│   ├── admin.tsx         # Componentes admin React (opcional)
│   └── astro/
│       └── index.ts
├── package.json
└── tsconfig.json

Configurar o pacote

{
	"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" }
}

Escrever o descritor e o runtime

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;

Regras de ID do plugin

O campo id deve corresponder a /^[a-z][a-z0-9_-]*$/.

Formato de versão

Use versionamento semântico.

Registrar o plugin

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

Tipos de campo: string, number, boolean, select, secret, url, email. Configurações são persistidas no mesmo armazenamento KV por plugin — leia-as com ctx.kv.get<T>("settings:<key>").

Exemplo completo — plugin de log de auditoria

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

interface AuditEntry {
	timestamp: string; action: "create" | "update" | "delete";
	collection: string; resourceId: 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: "Retenção (dias)", description: "Dias para manter entradas. 0 = para sempre.", 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) => {
					await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, {
						timestamp: new Date().toISOString(), action: event.isNew ? "create" : "update",
						collection: event.collection, resourceId: event.content.id as string,
					});
				},
			},
			"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;

Testes

  1. Crie um site de teste com EmDash instalado.
  2. Registre seu plugin em astro.config.mjs.
  3. Execute o servidor de desenvolvimento e acione hooks.
  4. Verifique o console e o armazenamento via rotas API.

Próximos passos