Seu primeiro plugin sandboxed

Nesta página

Este tutorial cria um plugin sandboxed que registra eventos de salvamento de conteúdo e expõe uma pequena rota de saúde. Você vai gerar o pacote com a CLI do plugin, adicionar um hook e uma rota, registrar o plugin em um site EmDash e confirmar que ambos os handlers são executados.

Se ainda não escolheu um formato de plugin, leia primeiro Escolher um formato de plugin.

Pré-requisitos

Você precisa de:

Gerar o plugin

  1. Execute o gerador do plugin a partir do diretório que conterá o novo projeto.

    pnpm dlx @emdash-cms/plugin-cli init save-log

    O comando pede o publisher, autor, contato de segurança e repositório fonte, depois mostra um resumo do projeto antes de criar esta estrutura:

    save-log/
    ├── .agents/
    │   └── skills -> ../skills
    ├── .claude/
    │   ├── CLAUDE.md -> ../AGENTS.md
    │   └── skills -> ../skills
    ├── AGENTS.md
    ├── emdash-plugin.jsonc
    ├── package.json
    ├── pnpm-workspace.yaml
    ├── README.md
    ├── skills/
    │   └── creating-plugins/SKILL.md
    ├── src/
    │   └── plugin.ts
    ├── tests/
    │   └── plugin.test.ts
    ├── tsconfig.json
    ├── vitest.config.ts
    └── .gitignore
  2. Instale as dependências do pacote gerado.

    cd save-log
    pnpm install

Definir o acesso e o storage do plugin

emdash-plugin.jsonc contém a identidade do plugin, informações do registry e o contrato de confiança. Adicione a capability content:read porque content:afterSave expõe o conteúdo salvo ao plugin. Declare uma collection de storage events para que o hook possa manter um registro consultável de cada salvamento.

O manifesto a seguir contém os campos usados neste tutorial. Mantenha os valores de publisher, author e security produzidos pelo gerador.

{
	"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",

	"slug": "save-log",
	"publisher": "did:plc:abc123def456",

	"license": "MIT",
	"author": { "name": "Jane Doe", "url": "https://example.com" },
	"security": { "email": "security@example.com" },
	"description": "Records content-save events.",

	"capabilities": ["content:read"],
	"allowedHosts": [],
	"storage": {
		"events": { "indexes": ["savedAt"] },
	},
}

A declaração content:read diz ao operador do site que o hook recebe conteúdo salvo e é necessária quando este formato de plugin é executado in-process. Acessar ctx.storage.events lançaria se a collection estivesse ausente. A referência do manifesto explica os demais campos e as regras de validação.

Adicionar o hook e a rota

Substitua o src/plugin.ts gerado pela seguinte definição de runtime:

import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	hooks: {
		"content:afterSave": {
			handler: async (event, ctx) => {
				const savedAt = new Date().toISOString();
				const contentId = String(event.content.id);
				await ctx.storage.events.put(`${savedAt}:${contentId}`, {
					savedAt,
					collection: event.collection,
					contentId,
				});

				ctx.log.info("Content save recorded", {
					collection: event.collection,
					contentId,
				});
			},
		},
	},

	routes: {
		health: {
			public: true,
			handler: async (_routeCtx, ctx) => {
				return { ok: true, plugin: ctx.plugin.id };
			},
		},
	},
};

export default plugin;

src/plugin.ts atribui a definição a uma constante tipada como SandboxedPlugin e a exporta como default. A anotação dá ao hook e à rota seus tipos de parâmetros sem adicionar o runtime EmDash ao bundle nem produzir caminhos de declaração específicos do gerenciador de pacotes.

Handlers de hooks recebem (event, ctx). Handlers de rotas recebem (routeCtx, ctx). A rota health é pública e somente leitura, então pode ser verificada sem uma sessão de administração. Rotas públicas são voltadas à internet; Rotas de API explica autenticação e regras de origin do navegador antes de expor dados reais ou mutações.

Atualizar o teste gerado

O teste gerado invoca a rota original hello pelo host de transporte Worker Loader. Substitua-o por um teste da rota health:

import { afterEach, describe, expect, it } from "vitest";

import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";

let host: PluginTestHost | undefined;

afterEach(async () => {
	await host?.dispose();
	host = undefined;
});

describe("health route", () => {
	it("identifies the running plugin", async () => {
		host = await createPluginTestHost();
		const result = await host.invokeRoute("health");
		expect(result).toEqual({ ok: true, plugin: "save-log" });
	});
});

O teste constrói o plugin e invoca a rota pelo Worker Loader e PluginBridge. O guia de teste de plugins sandboxed cobre hooks, fixtures de conteúdo, asserções de storage e os limites de testes workerd locais.

Validar e compilar

Execute o teste gerado, valide o manifesto e construa os artefatos npm.

pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build

A build cria:

  • dist/plugin.mjs, com o código do hook e da rota;
  • dist/manifest.json, com o manifesto de runtime e os nomes de hooks e rotas descobertos; e
  • dist/index.mjs, o descriptor exportado por padrão que um site importa.

dist/ é saída gerada. O gerador a exclui do Git porque a build do plugin a recria.

Registrar o plugin

Instale o pacote local no seu site EmDash. Execute este comando a partir do diretório do site e ajuste o caminho relativo se os projetos não forem irmãos.

pnpm add file:../save-log

Importe a exportação padrão gerada em astro.config.mjs e adicione-a a sandboxed:

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";

export default defineConfig({
	integrations: [
		emdash({
			sandboxed: [saveLog],
			sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
		}),
	],
});

Este exemplo usa o runner workerd do Node.js. Mantenha o runner já configurado pelo seu site se ele usar Cloudflare Workers ou outra configuração suportada.

Executar o plugin

Inicie ambos os processos de desenvolvimento:

  1. Execute pnpm dev no diretório do plugin. A CLI reconstrói o plugin quando a fonte ou o manifesto muda.
  2. Execute o comando de desenvolvimento do site no diretório do site.

Abra a seguinte rota no site:

http://localhost:4321/_emdash/api/plugins/save-log/health

A resposta contém o envelope padrão da API e o valor retornado pelo plugin:

{
	"success": true,
	"data": { "ok": true, "plugin": "save-log" },
}

Salve uma entrada no admin EmDash. O log do site contém Content save recorded, e o hook escreve um item na collection events do plugin.

Continuar construindo

  • Hooks explica eventos de hooks, capabilities, ordem e erros.
  • Rotas de API cobre validação, permissões, rotas públicas e exposição MCP.
  • Block Kit adiciona uma página de administração sem enviar JavaScript do navegador.
  • Configurações armazena configuração do plugin específica do site.
  • Storage cobre consultas indexadas e paginação.
  • Testing cobre testes de transporte sandbox diretos e testes de ações do host respaldados por runtime.
  • Empacotar e publicar publica o plugin no registry.