Seu primeiro plugin nativo

Nesta página

Um plugin nativo é um pacote npm que o EmDash importa no mesmo processo do site Astro. Este tutorial cria um plugin que registra em log os salvamentos de conteúdo, instala-o em um site e o registra em astro.config.mjs.

Use o formato nativo quando o plugin precisar de um recurso no mesmo processo, como componentes de administração em React, componentes de renderização do Astro ou fragmentos de página confiáveis. Se hooks, rotas, armazenamento e Block Kit cobrirem o recurso, comece com um plugin sandboxed. Escolher um formato de plugin compara os formatos.

Pré-requisitos

Comece com um site EmDash que use pnpm e consiga executar seu servidor de desenvolvimento.

Os comandos chamam o diretório do site de my-emdash-site e criam plugin-activity ao lado dele. Substitua my-emdash-site pelo nome do diretório do seu site.

Criar e registrar o pacote

  1. Crie um diretório de pacote nativo ao lado do site.

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. Adicione os metadados do pacote e os scripts de build.

    {
        "name": "@example/plugin-activity",
        "version": "0.1.0",
        "type": "module",
        "main": "./dist/index.mjs",
        "exports": {
            ".": {
                "types": "./dist/index.d.mts",
                "import": "./dist/index.mjs"
            }
        },
        "files": ["dist"],
        "scripts": {
            "build": "tsdown src/index.ts --format esm --dts --clean",
            "dev": "tsdown src/index.ts --format esm --dts --watch",
            "typecheck": "tsc --noEmit"
        },
        "peerDependencies": {
            "emdash": "*"
        },
        "devDependencies": {
            "emdash": "*",
            "tsdown": "^0.20.0",
            "typescript": "^5.9.0"
        }
    }

    O nome do pacote npm é @example/plugin-activity. O código de runtime abaixo usa o ID de plugin sem escopo plugin-activity para que ele caiba nas URLs das rotas de API do plugin.

  3. Adicione a configuração do TypeScript.

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "preserve",
            "moduleResolution": "bundler",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true,
            "outDir": "./dist",
            "rootDir": "./src"
        },
        "include": ["src/**/*"],
        "exclude": ["node_modules", "dist"]
    }
  4. Instale as dependências do pacote.

    pnpm install
  5. Crie src/index.ts com um hook de salvamento de conteúdo.

    import { definePlugin } from "emdash";
    import type { PluginDescriptor } from "emdash";
    
    export interface ActivityPluginOptions {
        logUpdates?: boolean;
    }
    
    export function activityPlugin(
        options: ActivityPluginOptions = {},
    ): PluginDescriptor<ActivityPluginOptions> {
        return {
            id: "plugin-activity",
            version: "0.1.0",
            format: "native",
            entrypoint: "@example/plugin-activity",
            options,
        };
    }
    
    export function createPlugin(options: ActivityPluginOptions = {}) {
        return definePlugin({
            id: "plugin-activity",
            version: "0.1.0",
            capabilities: ["content:read"],
            hooks: {
                "content:afterSave": async (event, ctx) => {
                    if (!event.isNew && options.logUpdates === false) return;
    
                    ctx.log.info("Content saved", {
                        collection: event.collection,
                        contentId: event.content.id,
                        isNew: event.isNew,
                    });
                },
            },
        });
    }
    
    export default createPlugin;

    content:afterSave exige a capability content:read. O EmDash ignora o hook quando essa capability está ausente.

  6. Faça o build do pacote.

    pnpm build
  7. Instale o pacote local no site.

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. Registre a factory do descritor na integração do EmDash.

    import { defineConfig } from "astro/config";
    import emdash from "emdash/astro";
    import { activityPlugin } from "@example/plugin-activity";
    
    export default defineConfig({
        integrations: [
            emdash({
                plugins: [activityPlugin({ logUpdates: true })],
            }),
        ],
    });

    Descritores nativos pertencem a plugins, não a sandboxed. O EmDash rejeita um descritor nativo no array sandboxed.

  9. Inicie o site e salve uma entrada no painel de administração.

    pnpm dev

    O log do servidor inclui Content saved com a coleção, o ID do conteúdo e se a entrada foi criada.

Limite entre descritor e runtime

A exportação do pacote tem duas funções. O EmDash usa cada uma em uma etapa diferente:

  • A factory do descritor, activityPlugin(), é executada enquanto o Astro avalia sua configuração. Ela retorna metadados serializáveis de build: id, version, format, entrypoint e options. Os pontos de entrada de React e Astro também pertencem a esse descritor.
  • A exportação nomeada createPlugin() é executada quando o EmDash inicializa. O EmDash a importa de entrypoint, passa as options serializadas e espera um plugin resolvido de definePlugin().

A exportação nomeada createPlugin é obrigatória. Uma exportação padrão pode ser útil para quem consome o pacote, mas o carregador nativo do EmDash importa createPlugin pelo nome.

Mantenha id e version idênticos no descritor e em definePlugin(). Use um ID de plugin sem escopo, em kebab-case, como plugin-activity; mantenha o escopo do npm no nome do pacote e em entrypoint. Assim o ID continua utilizável como único segmento de plugin nas URLs das rotas de API.

Identidade e versionamento do plugin lista as formas aceitas de ID e de versão.

O comportamento de runtime pertence a definePlugin():

  • capabilities e allowedHosts
  • storage
  • hooks e routes
  • declarações de admin para configurações, páginas, widgets e Portable Text

O descritor carrega as entradas estáticas que o Astro precisa importar ou expor em tempo de build. Os guias específicos mostram quais campos de administração precisam de declarações correspondentes no descritor e no runtime.

Handlers de rotas nativos

Os handlers de rotas nativos recebem um único RouteContext. Ele combina a entrada validada e os dados da requisição com o PluginContext habitual:

routes: {
	status: {
		permission: "plugins:read",
		handler: async (ctx) => ({
			pluginId: ctx.plugin.id,
			callerId: ctx.user?.id ?? null,
		}),
	},
},

O handler sandboxed equivalente recebe (routeCtx, ctx) como dois argumentos. Autenticação, permissões, schemas de entrada e URLs de rotas seguem, no restante, o contrato compartilhado das rotas de API.

Envolva uma rota nativa em definePluginRoute() quando ela declarar request.body; o helper infere ctx.input a partir do modo do corpo. Uma rota nativa com response: "raw" retorna pluginResponse(). Importe ambos os helpers de emdash. O guia compartilhado de rotas de API lista os modos de corpo, os limites, a política de resposta e os padrões de compatibilidade.

Ler secrets e bindings

Os handlers de hooks e de rotas em um plugin nativo recebem o contexto do plugin, não o contexto do Astro, portanto Astro.locals não está disponível neles. Leia secrets e bindings da plataforma a partir do ambiente de runtime.

Leia os secrets de implantação em process.env. No Node.js, o gerenciador de secrets da plataforma de hospedagem os fornece. No Cloudflare Workers, os secrets definidos com wrangler secret put chegam a process.env por meio do flag nodejs_compat com uma data de compatibilidade de 2025-04-01 ou posterior, conforme descrito em Secrets do Worker.

Para uma credencial que um administrador do site informa, declare em vez disso um campo secret no formulário de configurações gerado. O EmDash criptografa o valor com EMDASH_ENCRYPTION_KEY antes de armazená-lo, portanto defina essa chave antes de um administrador salvar o campo. O plugin lê o valor com ctx.settings.get().

No Cloudflare Workers, bindings como uma fila, um bucket R2 ou send_email são objetos em env de cloudflare:workers. O Astro carrega astro.config.mjs no Node.js, e esse arquivo importa o pacote do plugin para a factory do descritor. O Node.js não consegue carregar cloudflare:workers, então importe-o dentro do handler em vez de no topo do módulo. O hook a seguir envia o ID de cada entrada salva para uma fila vinculada como ACTIVITY_QUEUE:

hooks: {
	"content:afterSave": async (event) => {
		const { env } = await import("cloudflare:workers");
		await env.ACTIVITY_QUEUE.send({ contentId: event.content.id });
	},
},

O módulo cloudflare:workers só é resolvido no runtime do Workers, portanto um plugin que o importa só funciona em sites que usam o adaptador da Cloudflare.

Para verificar os tipos da importação, adicione @cloudflare/workers-types às devDependencies do pacote e a compilerOptions.types em tsconfig.json. Depois declare cada binding que o plugin lê na interface Cloudflare.Env. A declaração a seguir tipa a fila do exemplo anterior:

declare namespace Cloudflare {
	interface Env {
		ACTIVITY_QUEUE: Queue;
	}
}

Adicionar outra superfície