Il tuo primo plugin nativo

In questa pagina

Un plugin nativo è un pacchetto npm che EmDash importa nello stesso processo del sito Astro. Questo tutorial crea un plugin che registra i salvataggi dei contenuti, lo installa in un sito e lo registra in astro.config.mjs.

Usa il formato nativo quando il plugin ha bisogno di una funzionalità in-process, come componenti di amministrazione React, componenti di rendering Astro o frammenti di pagina attendibili. Se hook, route, storage e Block Kit coprono la funzionalità, inizia con un plugin sandboxed. Scegliere un formato di plugin confronta i formati.

Prerequisiti

Parti da un sito EmDash che usa pnpm e può eseguire il proprio server di sviluppo.

I comandi chiamano la directory del sito my-emdash-site e creano plugin-activity accanto a essa. Sostituisci my-emdash-site con il nome della directory del tuo sito.

Creare e registrare il pacchetto

  1. Crea una directory per il pacchetto nativo accanto al sito.

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. Aggiungi i metadati del pacchetto e gli script di 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"
        }
    }

    Il nome del pacchetto npm è @example/plugin-activity. Il codice di runtime qui sotto usa l’ID del plugin senza scope plugin-activity, in modo che si adatti agli URL delle route API del plugin.

  3. Aggiungi la configurazione 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. Installa le dipendenze del pacchetto.

    pnpm install
  5. Crea src/index.ts con un hook di salvataggio dei contenuti.

    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 richiede la capability content:read. EmDash salta l’hook quando manca quella capability.

  6. Compila il pacchetto.

    pnpm build
  7. Installa il pacchetto locale nel sito.

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. Registra la factory del descrittore nell’integrazione 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 })],
            }),
        ],
    });

    I descrittori nativi vanno in plugins, non in sandboxed. EmDash rifiuta un descrittore nativo nell’array sandboxed.

  9. Avvia il sito e salva una voce nel pannello di amministrazione.

    pnpm dev

    Il log del server include Content saved con la collezione, l’ID del contenuto e l’indicazione se la voce è stata creata.

Confine tra descrittore e runtime

L’export del pacchetto ha due compiti. EmDash usa ciascuno in una fase diversa:

  • La factory del descrittore, activityPlugin(), viene eseguita mentre Astro valuta la propria configurazione. Restituisce metadati serializzabili di build: id, version, format, entrypoint e options. Anche i punti di ingresso React e Astro appartengono a questo descrittore.
  • L’export con nome createPlugin() viene eseguito quando EmDash si inizializza. EmDash lo importa da entrypoint, gli passa le options serializzate e si aspetta un plugin risolto da definePlugin().

L’export con nome createPlugin è obbligatorio. Un export predefinito può essere utile a chi consuma il pacchetto, ma il loader nativo di EmDash importa createPlugin per nome.

Mantieni id e version identici nel descrittore e in definePlugin(). Usa un ID di plugin senza scope in kebab-case, come plugin-activity; mantieni lo scope npm nel nome del pacchetto e in entrypoint. Così l’ID resta utilizzabile come unico segmento di plugin negli URL delle route API.

Identità e versionamento del plugin elenca le forme accettate per ID e versione.

Il comportamento di runtime appartiene a definePlugin():

  • capabilities e allowedHosts
  • storage
  • hooks e routes
  • dichiarazioni admin per impostazioni, pagine, widget e Portable Text

Il descrittore trasporta le voci statiche che Astro deve importare o esporre in fase di build. Le guide specifiche mostrano quali campi di amministrazione richiedono dichiarazioni corrispondenti nel descrittore e nel runtime.

Handler di route nativi

Gli handler di route nativi ricevono un unico RouteContext. Combina input validato e dati della richiesta con il normale PluginContext:

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

L’handler sandboxed equivalente riceve (routeCtx, ctx) come due argomenti. Autenticazione, permessi, schemi di input e URL delle route seguono per il resto il contratto condiviso delle route API.

Avvolgi una route nativa in definePluginRoute() quando dichiara request.body; l’helper deduce ctx.input dalla modalità del body. Una route nativa con response: "raw" restituisce pluginResponse(). Importa entrambi gli helper da emdash. La guida condivisa alle route API elenca le modalità del body, i limiti, la policy di risposta e i valori predefiniti di compatibilità.

Leggere secret e binding

Gli handler di hook e route in un plugin nativo ricevono il contesto del plugin, non il contesto Astro, quindi Astro.locals non è disponibile al loro interno. Leggi secret e binding della piattaforma dall’ambiente di runtime.

Leggi i secret di distribuzione da process.env. Su Node.js li fornisce il gestore dei secret della piattaforma di hosting. Su Cloudflare Workers, i secret impostati con wrangler secret put raggiungono process.env tramite il flag nodejs_compat con una data di compatibilità del 2025-04-01 o successiva, come descritto in Secret del Worker.

Per una credenziale che inserisce un amministratore del sito, dichiara invece un campo secret nel modulo delle impostazioni generato. EmDash cifra il valore con EMDASH_ENCRYPTION_KEY prima di memorizzarlo, quindi imposta quella chiave prima che un amministratore salvi il campo. Il plugin legge il valore con ctx.settings.get().

Su Cloudflare Workers, i binding come una coda, un bucket R2 o send_email sono oggetti su env da cloudflare:workers. Astro carica astro.config.mjs in Node.js, e quel file importa il pacchetto del plugin per la factory del descrittore. Node.js non può caricare cloudflare:workers, quindi importalo dentro l’handler anziché in cima al modulo. Il hook seguente invia l’ID di ogni voce salvata a una coda associata come ACTIVITY_QUEUE:

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

Il modulo cloudflare:workers si risolve solo nel runtime Workers, quindi un plugin che lo importa funziona solo sui siti che usano l’adattatore Cloudflare.

Per controllare i tipi dell’import, aggiungi @cloudflare/workers-types alle devDependencies del pacchetto e a compilerOptions.types in tsconfig.json. Poi dichiara ogni binding letto dal plugin sull’interfaccia Cloudflare.Env. La dichiarazione seguente tipizza la coda dell’esempio precedente:

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

Aggiungere un’altra superficie