Este guia orienta você na construção de um plugin sandboxed mínimo do zero — um plugin que registra cada salvamento de conteúdo e expõe uma única rota API. No final, você terá um plugin que é executado em um runtime isolado através do sandbox runner configurado. O mesmo código do plugin também pode ser executado in-process se o operador do site optar por movê-lo de sandboxed: [] para plugins: [] — por exemplo, em plataformas sem um sandbox runner disponível.
Se você ainda não decidiu se deseja um plugin sandboxed ou nativo, leia primeiro Escolhendo um formato de plugin.
Dois arquivos
Cada plugin sandboxed consiste em duas peças:
- Um descritor — um pequeno objeto descrevendo o plugin (id, versão, capacidades, armazenamento, onde encontrar a entrada de runtime). Importado por
astro.config.mjsno momento da compilação. - Uma entrada sandbox — o código de runtime: hooks, rotas, acesso ao armazenamento. Carregado no runtime sandbox no momento da requisição.
Os dois arquivos vivem no mesmo pacote e são executados em ambientes completamente diferentes. O descritor nunca vê o contexto de runtime; a entrada nunca vê astro.config.mjs.
my-plugin/
├── src/
│ ├── index.ts # Descritor — executado no Vite no momento da compilação
│ └── sandbox-entry.ts # Hooks, rotas, armazenamento — executado no runtime sandbox
├── package.json
└── tsconfig.json
Configurar o pacote
-
Crie um novo diretório e inicialize-o como um pacote de módulo TypeScript ES.
{ "name": "@my-org/plugin-hello", "version": "0.1.0", "type": "module", "main": "dist/index.mjs", "exports": { ".": { "import": "./dist/index.mjs", "types": "./dist/index.d.mts" }, "./sandbox": "./dist/sandbox-entry.mjs" }, "files": ["dist"], "scripts": { "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean" }, "peerDependencies": { "emdash": "*" }, "devDependencies": { "emdash": "*", "tsdown": "^0.6.0", "typescript": "^5.5.0" } }A exportação
"./sandbox"é para onde oentrypointdo descritor apontará. O bundler constrói ambos os arquivos emdist/. -
Adicione um
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Escrever o descritor
O descritor é uma função factory que retorna um PluginDescriptor. Ele é executado no Vite no momento da compilação, o que significa que deve ser livre de efeitos colaterais e não pode usar nenhuma API de runtime (fetch, o banco de dados, variáveis de ambiente — nenhuma delas existe ainda).
import type { PluginDescriptor } from "emdash";
export function helloPlugin(): PluginDescriptor {
return {
id: "plugin-hello",
version: "0.1.0",
format: "standard",
entrypoint: "@my-org/plugin-hello/sandbox",
capabilities: [],
storage: {
events: { indexes: ["timestamp"] },
},
};
}
Alguns detalhes importantes:
format: "standard"é obrigatório. Sem ele, EmDash trata o pacote como um plugin nativo e procura por uma forma diferente. O campoformaté"native"por padrão.entrypointé um especificador de módulo, não um caminho de arquivo. Use a mesma string que você passaria paraimport— geralmente"<package-name>/sandbox". O nome do pacote pode ter escopo (@my-org/plugin-hello); oiddo plugin não pode.idé um slug seguro para URL, não o nome do pacote npm. Ele 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 tanto como um único segmento de caminho nas URLs de rota do plugin (/_emdash/api/plugins/<id>/...) quanto como parte dos identificadores SQL gerados para os índices de armazenamento do plugin, portanto@,/, dígitos iniciais e letras maiúsculas falham todos no runtime. Combine umidsem escopo comoplugin-hellocom um nome de pacote npm com escopo ementrypoint.- Capacidades, allowedHosts e armazenamento residem no descritor. A entrada sandbox não os declara — ela só pode usar o que o descritor permite.
- Não coloque lógica de runtime aqui. Sem
awaitde nível superior, semfetchno nível do módulo, sem leitura de arquivos. O descritor são metadados.
Escrever a entrada sandbox
O lado do runtime. Este arquivo é carregado no runtime sandbox no momento da requisição, sem acesso a nada exceto o que ctx fornece.
import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";
interface ContentSaveEvent {
collection: string;
content: { id: string };
isNew: boolean;
}
export default definePlugin({
hooks: {
"content:afterSave": {
handler: async (event: ContentSaveEvent, ctx: PluginContext) => {
ctx.log.info("Content saved", {
collection: event.collection,
id: event.content.id,
});
await ctx.storage.events.put(`save-${Date.now()}`, {
timestamp: new Date().toISOString(),
collection: event.collection,
contentId: event.content.id,
});
},
},
},
routes: {
recent: {
handler: async (_routeCtx, ctx: PluginContext) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
});
Coisas que vale a pena saber:
definePlugin()na entrada sandbox aceita apenas{ hooks, routes }. Semid, semversion, semcapabilities— estes vêm do descritor. EmDash lança um erro no momento da compilação se você tentar passá-los aqui.- Handlers de hooks aceitam
(event, ctx). A forma do evento depende do nome do hook; veja a Referência de hooks. - Handlers de rotas aceitam
(routeCtx, ctx)— dois argumentos.routeCtxtem{ input, request, requestMeta };ctxé o mesmoPluginContextque você obtém nos hooks. As rotas são acessíveis em/_emdash/api/plugins/<plugin-id>/<route-name>. ctx.storage.eventsfunciona porqueeventsfoi declarado no descritor. Acessar uma coleção não declarada lança um erro.ctx.kvestá sempre disponível — um armazenamento de chave-valor por plugin comget,set,deleteelist(prefix).
Registrar o plugin
No astro.config.mjs do seu site, importe a factory do descritor e passe-a para a integração EmDash. Plugins sandboxed vão em sandboxed: []; plugins in-process vão em plugins: []. Um plugin de formato padrão funciona em ambos — comece com sandboxed.
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import { helloPlugin } from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [helloPlugin()],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner é a peça plugável. O exemplo acima usa sandbox() de @emdash-cms/cloudflare, que é o sandbox runner que a maioria dos sites usa hoje. Runners para outras plataformas estão em desenvolvimento. Se nenhum runner estiver configurado (ou o runner configurado reportar como indisponível na plataforma atual), plugins sandboxed: [] são ignorados na inicialização — para executar o mesmo plugin in-process, mova-o de sandboxed: [] para plugins: [].
Construir e executar
Do diretório do plugin:
pnpm build
No diretório do site, vincule ou instale o plugin (pnpm add @my-org/plugin-hello ou um link de workspace), depois inicie o servidor de desenvolvimento. Você deve ver [hello] Content saved … nos logs na próxima vez que salvar um conteúdo no admin, e GET /_emdash/api/plugins/plugin-hello/recent deve retornar os últimos dez eventos de salvamento.
O que ler a seguir
- Hooks — o conjunto completo de eventos aos quais você pode reagir
- Rotas API — validação de entrada, rotas públicas, tratamento de erros
- Armazenamento e KV — opções de consulta, índices, operações em lote
- Capacidades e segurança — acesso a conteúdo, requisições de rede, listas brancas de hosts
- Empacotamento e publicação — quando você estiver pronto para publicar no marketplace