Votre premier plugin natif

Sur cette page

Un plugin natif est un paquet npm qu’EmDash importe dans le même processus que le site Astro. Ce tutoriel crée un plugin qui journalise les enregistrements de contenu, l’installe dans un site et l’enregistre dans astro.config.mjs.

Utilisez le format natif lorsque le plugin a besoin d’une fonctionnalité dans le même processus, comme des composants d’administration React, des composants de rendu Astro ou des fragments de page de confiance. Si les hooks, les routes, le stockage et Block Kit couvrent la fonctionnalité, commencez par un plugin sandboxed. Choisir un format de plugin compare les formats.

Prérequis

Partez d’un site EmDash qui utilise pnpm et peut exécuter son serveur de développement.

Les commandes appellent le répertoire du site my-emdash-site et créent plugin-activity à côté. Remplacez my-emdash-site par le nom du répertoire de votre site.

Créer et enregistrer le paquet

  1. Créez un répertoire de paquet natif à côté du site.

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. Ajoutez les métadonnées du paquet et les 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"
        }
    }

    Le nom du paquet npm est @example/plugin-activity. Le code d’exécution ci-dessous utilise l’ID de plugin sans portée plugin-activity afin qu’il tienne dans les URL des routes d’API du plugin.

  3. Ajoutez la configuration 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. Installez les dépendances du paquet.

    pnpm install
  5. Créez src/index.ts avec un hook d’enregistrement de contenu.

    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 nécessite la capacité content:read. EmDash ignore le hook lorsque cette capacité est absente.

  6. Construisez le paquet.

    pnpm build
  7. Installez le paquet local dans le site.

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. Enregistrez la fabrique de descripteur dans l’intégration 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 })],
            }),
        ],
    });

    Les descripteurs natifs vont dans plugins, pas dans sandboxed. EmDash rejette un descripteur natif placé dans le tableau sandboxed.

  9. Démarrez le site et enregistrez une entrée dans le panneau d’administration.

    pnpm dev

    Le journal du serveur inclut Content saved avec la collection, l’ID du contenu et l’indication de création de l’entrée.

Frontière entre le descripteur et l’exécution

L’export du paquet remplit deux rôles. EmDash utilise chacun à une étape différente :

  • La fabrique de descripteur, activityPlugin(), s’exécute pendant qu’Astro évalue sa configuration. Elle renvoie des métadonnées sérialisables de build : id, version, format, entrypoint et options. Les points d’entrée React et Astro appartiennent aussi à ce descripteur.
  • L’export nommé createPlugin() s’exécute lorsque EmDash s’initialise. EmDash l’importe depuis entrypoint, lui transmet les options sérialisées et attend un plugin résolu de definePlugin().

L’export nommé createPlugin est obligatoire. Un export par défaut peut être utile aux consommateurs du paquet, mais le chargeur natif d’EmDash importe createPlugin par son nom.

Gardez id et version identiques dans le descripteur et dans definePlugin(). Utilisez un ID de plugin sans portée, en kebab-case, comme plugin-activity ; conservez la portée npm dans le nom du paquet et dans entrypoint. Cela permet d’utiliser l’ID comme unique segment de plugin dans les URL des routes d’API.

Identité et versionnage du plugin liste les formes d’ID et de version acceptées.

Le comportement d’exécution appartient à definePlugin() :

  • capabilities et allowedHosts
  • storage
  • hooks et routes
  • les déclarations admin : paramètres, page, widget et Portable Text

Le descripteur porte les entrées statiques qu’Astro doit importer ou exposer au moment du build. Les guides spécialisés indiquent quels champs d’administration nécessitent des déclarations concordantes dans le descripteur et à l’exécution.

Gestionnaires de routes natifs

Les gestionnaires de routes natifs reçoivent un seul RouteContext. Il combine l’entrée validée et les données de la requête avec le PluginContext habituel :

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

Le gestionnaire sandboxed équivalent reçoit (routeCtx, ctx) comme deux arguments. L’authentification, les permissions, les schémas d’entrée et les URL de routes suivent par ailleurs le contrat commun des routes d’API.

Enveloppez une route native dans definePluginRoute() lorsqu’elle déclare request.body ; l’assistant déduit ctx.input du mode du corps. Une route native avec response: "raw" renvoie pluginResponse(). Importez les deux assistants depuis emdash. Le guide commun des routes d’API liste les modes de corps, les limites, la politique de réponse et les valeurs par défaut de compatibilité.

Lire les secrets et les bindings

Les gestionnaires de hooks et de routes d’un plugin natif reçoivent le contexte du plugin, pas le contexte Astro ; Astro.locals n’y est donc pas disponible. Lisez les secrets et les bindings de plateforme depuis l’environnement d’exécution.

Lisez les secrets de déploiement depuis process.env. Sur Node.js, le gestionnaire de secrets de la plateforme d’hébergement les fournit. Sur Cloudflare Workers, les secrets définis avec wrangler secret put atteignent process.env grâce au flag nodejs_compat avec une date de compatibilité du 2025-04-01 ou ultérieure, comme décrit dans Secrets du Worker.

Pour un identifiant saisi par un administrateur du site, déclarez plutôt un champ secret dans le formulaire de paramètres généré. EmDash chiffre la valeur avec EMDASH_ENCRYPTION_KEY avant de la stocker ; définissez donc cette clé avant qu’un administrateur n’enregistre le champ. Le plugin lit la valeur avec ctx.settings.get().

Sur Cloudflare Workers, les bindings tels qu’une file d’attente, un bucket R2 ou send_email sont des objets de env dans cloudflare:workers. Astro charge astro.config.mjs dans Node.js, et ce fichier importe le paquet du plugin pour la fabrique de descripteur. Node.js ne peut pas charger cloudflare:workers ; importez-le donc dans le gestionnaire plutôt qu’en haut du module. Le hook suivant envoie l’ID de chaque entrée enregistrée à une file d’attente liée sous le nom ACTIVITY_QUEUE :

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

Le module cloudflare:workers ne se résout que dans l’environnement d’exécution Workers ; un plugin qui l’importe ne fonctionne donc que sur les sites qui utilisent l’adaptateur Cloudflare.

Pour vérifier les types de l’import, ajoutez @cloudflare/workers-types aux devDependencies du paquet et à compilerOptions.types dans tsconfig.json. Déclarez ensuite chaque binding lu par le plugin dans l’interface Cloudflare.Env. La déclaration suivante type la file d’attente de l’exemple précédent :

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

Ajouter une autre surface