Este guia constrói um plugin sandboxed mínimo do zero. O plugin registra cada salvamento de conteúdo e expõe uma única rota de API. Ele é executado em um runtime isolado fornecido pelo sandbox runner configurado. O mesmo código também é executado em processo quando um operador do site o move de sandboxed: [] para plugins: [], por exemplo em uma plataforma sem sandbox runner.
Se você ainda não decidiu entre sandboxed e native, leia primeiro Escolhendo um formato de plugin.
Duas peças
Um plugin sandboxed é:
emdash-plugin.jsonc— um manifest editado manualmente: identidade, o contrato de confiança (capabilities, hosts, storage), e campos de perfil. Sem código.src/plugin.ts— o runtime: hooks e routes. Imports apenas de tipos deemdash/plugin; sem import runtime deemdash.
emdash-plugin build lê ambos e emite os artefatos dist/ que um site consome.
O exemplo a seguir mostra o layout de arquivos de um plugin completo:
my-plugin/
├── emdash-plugin.jsonc # Identidade + contrato de confiança + perfil
├── src/
│ └── plugin.ts # Hooks, routes — executa no runtime sandbox
├── package.json
└── tsconfig.json
Configurar o pacote
-
Crie o diretório e um
package.json. O build éemdash-plugin build; não há invocaçãotsdownpara escrever.{ "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/plugin.mjs" }, "files": ["dist", "emdash-plugin.jsonc"], "scripts": { "build": "emdash-plugin build", "dev": "emdash-plugin dev" }, "peerDependencies": { "emdash": ">=0.13.0" }, "devDependencies": { "@emdash-cms/plugin-cli": "0.2.0", "emdash": ">=0.13.0", "typescript": "^5.9.0" } }"."é o descritor gerado que um site importa;"./sandbox"é o arquivo runtime construído.emdash-plugin buildgera ambos. -
Adicione um
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipLibCheck": true, "types": [] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
Escrever o manifest
emdash-plugin.jsonc carrega a identidade do plugin (slug), seu contrato de confiança (capabilities, allowedHosts, storage), campos de perfil, e o publisher pin.
O exemplo a seguir mostra um manifest completo para o plugin hello:
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "plugin-hello",
"publisher": "did:plc:abc123def456", // your Atmosphere account DID
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"capabilities": [],
"allowedHosts": [],
"storage": { "events": { "indexes": ["timestamp"] } }
}
Notas sobre este manifest:
slugé um id seguro para URL, não o nome do pacote npm./^[a-z][a-z0-9_-]*$/, máx 64 caracteres. É um único segmento de caminho nas URLs de rotas do plugin (/_emdash/api/plugins/<slug>/...) e parte dos identificadores SQL gerados para índices de storage, então@,/, dígitos iniciais e maiúsculas falham. Combine umslugsem escopo (plugin-hello) com um nome de pacote npm com escopo.storagedeclara coleções antecipadamente.ctx.storage.eventsfunciona em runtime apenas porqueeventsestá declarado aqui. Acessar uma coleção não declarada lança um erro.versioné omitida. O build a lê depackage.jsonpara que haja uma única fonte de verdade. Veja a referência do manifest.- O contrato de confiança é consentimento. Alterar
capabilities,allowedHosts, oustorageposteriormente requer um bump de versão — sites instalados consentiram com o contrato antigo.
Escrever o runtime
src/plugin.ts exporta por padrão um objeto simples anotado com satisfies SandboxedPlugin. emdash/plugin fornece apenas tipos, então um plugin sandboxed não tem dependência runtime em emdash.
O exemplo a seguir registra cada salvamento de conteúdo no storage do plugin e expõe uma rota recent que retorna os últimos dez salvamentos:
import type { SandboxedPlugin } from "emdash/plugin";
export default {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
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) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
} satisfies SandboxedPlugin;
Notas sobre o arquivo runtime:
satisfies SandboxedPlugintipifica tudo. Ele infereeventdo nome do hook (com o tipo de evento canônico completo) ectxcomoPluginContext, então os handlers não precisam de anotações de parâmetros. Uma chave de hook com erro de digitação como"content:afterSav"é um erro de compilação.- Handlers de hooks recebem
(event, ctx). A forma do evento depende do nome do hook; veja o guia de Hooks. - Handlers de routes recebem
(routeCtx, ctx)— dois argumentos.routeCtxé{ input, request, requestMeta? };ctxé o mesmoPluginContext. Routes são acessíveis em/_emdash/api/plugins/<slug>/<route-name>. ctx.storage.eventsfunciona porqueeventsestá declarado no manifest.ctx.kvestá sempre disponível — um key-value store por plugin comget,set,delete,list(prefix).
Registrar o plugin
No astro.config.mjs do site, importe a exportação padrão do plugin e passe-a. Plugins sandboxed vão em sandboxed: []; plugins em processo vão em plugins: []. Um plugin sandboxed funciona em ambos. O exemplo abaixo usa sandboxed::
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import hello from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [hello],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner é a peça intercambiável. O exemplo usa sandbox() de @emdash-cms/cloudflare, o runner que a maioria dos sites usa hoje. Se nenhum runner estiver configurado (ou o runner configurado não estiver disponível na plataforma atual), plugins sandboxed: [] são pulados na inicialização — mova o plugin para plugins: [] para executá-lo em processo.
Construir e executar
Do diretório do plugin:
emdash-plugin validate # verifica o schema do manifest primeiro
emdash-plugin build # emite dist/
Para um loop de edição, execute emdash-plugin dev (reconstrói ao salvar, mantém o último dist/ bom em um build com falha). No site, instale ou vincule o plugin (pnpm add file:../plugin-hello ou um link de workspace) e inicie o servidor dev. Salve um conteúdo no admin e você deve ver Content saved … nos logs; GET /_emdash/api/plugins/plugin-hello/recent retorna os últimos dez eventos de salvamento.
O que ler a seguir
- O manifest — cada campo, o contrato de confiança, publisher pinning
- A CLI
emdash-plugin—build,dev,bundle - Hooks — o conjunto completo de eventos
- Rotas de API — validação de input, rotas públicas, erros
- Storage e KV — opções de query, índices, operações em lote
- Capabilities e segurança — acesso a conteúdo, rede, allowlists de hosts
- Bundling e publishing — envio para o marketplace