Tu primer plugin nativo

En esta página

Un plugin nativo es un paquete de npm que EmDash importa en el mismo proceso que el sitio de Astro. Este tutorial crea un plugin que registra los guardados de contenido, lo instala en un sitio y lo registra en astro.config.mjs.

Usa el formato nativo cuando el plugin necesite una función dentro del proceso, como componentes de administración en React, componentes de renderizado de Astro o fragmentos de página de confianza. Si los hooks, las rutas, el almacenamiento y Block Kit cubren la función, empieza con un plugin sandboxed. Elegir un formato de plugin compara los formatos.

Requisitos previos

Empieza con un sitio EmDash que use pnpm y pueda ejecutar su servidor de desarrollo.

Los comandos llaman my-emdash-site al directorio del sitio y crean plugin-activity junto a él. Sustituye my-emdash-site por el nombre del directorio de tu sitio.

Crear y registrar el paquete

  1. Crea un directorio de paquete nativo junto al sitio.

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. Añade los metadatos del paquete y los scripts de compilación.

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

    El nombre del paquete de npm es @example/plugin-activity. El código de ejecución que aparece más abajo usa el ID de plugin sin ámbito plugin-activity para que quepa en las URL de las rutas de la API del plugin.

  3. Añade la configuración de 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. Instala las dependencias del paquete.

    pnpm install
  5. Crea src/index.ts con un hook de guardado de contenido.

    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 requiere la capacidad content:read. EmDash omite el hook cuando falta esa capacidad.

  6. Compila el paquete.

    pnpm build
  7. Instala el paquete local en el sitio.

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. Registra la factoría del descriptor en la integración de 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 })],
            }),
        ],
    });

    Los descriptores nativos van en plugins, no en sandboxed. EmDash rechaza un descriptor nativo en el array sandboxed.

  9. Inicia el sitio y guarda una entrada en el panel de administración.

    pnpm dev

    El registro del servidor incluye Content saved con la colección, el ID del contenido y si la entrada se creó.

Límite entre el descriptor y el entorno de ejecución

La exportación del paquete tiene dos funciones. EmDash usa cada una en una etapa distinta:

  • La factoría del descriptor, activityPlugin(), se ejecuta mientras Astro evalúa su configuración. Devuelve metadatos serializables de tiempo de compilación: id, version, format, entrypoint y options. Los puntos de entrada de React y Astro también pertenecen a este descriptor.
  • La exportación con nombre createPlugin() se ejecuta cuando EmDash se inicializa. EmDash la importa desde entrypoint, le pasa las options serializadas y espera un plugin resuelto de definePlugin().

La exportación con nombre createPlugin es obligatoria. Una exportación por defecto puede resultar útil a quienes consumen el paquete, pero el cargador nativo de EmDash importa createPlugin por nombre.

Mantén id y version idénticos en el descriptor y en definePlugin(). Usa un ID de plugin sin ámbito y en kebab-case, como plugin-activity; conserva el ámbito de npm en el nombre del paquete y en entrypoint. Así el ID sigue siendo utilizable como único segmento de plugin en las URL de las rutas de la API.

Identidad y versionado del plugin enumera las formas de ID y de versión aceptadas.

El comportamiento en tiempo de ejecución pertenece a definePlugin():

  • capabilities y allowedHosts
  • storage
  • hooks y routes
  • declaraciones de admin: ajustes, páginas, widgets y Portable Text

El descriptor lleva las entradas estáticas que Astro debe importar o exponer en tiempo de compilación. Las guías específicas muestran qué campos de administración necesitan declaraciones coincidentes en el descriptor y en el entorno de ejecución.

Manejadores de rutas nativos

Los manejadores de rutas nativos reciben un único RouteContext. Combina la entrada validada y los datos de la solicitud con el PluginContext habitual:

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

El manejador sandboxed equivalente recibe (routeCtx, ctx) como dos argumentos. La autenticación, los permisos, los esquemas de entrada y las URL de las rutas siguen, por lo demás, el contrato compartido de las rutas de API.

Envuelve una ruta nativa en definePluginRoute() cuando declare request.body; el helper infiere ctx.input a partir del modo del cuerpo. Una ruta nativa con response: "raw" devuelve pluginResponse(). Importa ambos helpers desde emdash. La guía compartida de rutas de API enumera los modos de cuerpo, los límites, la política de respuesta y los valores predeterminados de compatibilidad.

Leer secretos y bindings

Los manejadores de hooks y de rutas de un plugin nativo reciben el contexto del plugin, no el contexto de Astro, por lo que Astro.locals no está disponible en ellos. Lee los secretos y los bindings de la plataforma desde el entorno de ejecución.

Lee los secretos de despliegue desde process.env. En Node.js, los proporciona el gestor de secretos de la plataforma de alojamiento. En Cloudflare Workers, los secretos establecidos con wrangler secret put llegan a process.env mediante el flag nodejs_compat con una fecha de compatibilidad de 2025-04-01 o posterior, como se describe en Secretos del Worker.

Para una credencial que introduce un administrador del sitio, declara en su lugar un campo secret en el formulario de ajustes generado. EmDash cifra el valor con EMDASH_ENCRYPTION_KEY antes de almacenarlo, así que define esa clave antes de que un administrador guarde el campo. El plugin lee el valor con ctx.settings.get().

En Cloudflare Workers, los bindings como una cola, un bucket de R2 o send_email son objetos de env en cloudflare:workers. Astro carga astro.config.mjs en Node.js, y ese archivo importa el paquete del plugin para la factoría del descriptor. Node.js no puede cargar cloudflare:workers, así que impórtalo dentro del manejador en lugar de al principio del módulo. El siguiente hook envía el ID de cada entrada guardada a una cola enlazada como ACTIVITY_QUEUE:

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

El módulo cloudflare:workers solo se resuelve en el entorno de ejecución de Workers, por lo que un plugin que lo importa solo funciona en sitios que usan el adaptador de Cloudflare.

Para comprobar los tipos de la importación, añade @cloudflare/workers-types a las devDependencies del paquete y a compilerOptions.types en tsconfig.json. Después declara cada binding que lee el plugin en la interfaz Cloudflare.Env. La siguiente declaración tipa la cola del ejemplo anterior:

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

Añadir otra superficie