Il tuo primo plugin sandboxed

In questa pagina

Questa guida costruisce un plugin sandboxed minimale da zero. Il plugin registra ogni salvataggio di contenuto ed espone una singola route API. Viene eseguito in un runtime isolato fornito dal sandbox runner configurato. Lo stesso codice viene eseguito anche in-process quando un operatore del sito lo sposta da sandboxed: [] a plugins: [], ad esempio su una piattaforma senza sandbox runner.

Se non hai ancora deciso tra sandboxed e native, leggi prima Scegliere un formato di plugin.

Due pezzi

Un plugin sandboxed è:

  1. emdash-plugin.jsonc — un manifest modificato manualmente: identità, il contratto di fiducia (capabilities, hosts, storage), e campi profilo. Nessun codice.
  2. src/plugin.ts — il runtime: hooks e routes. Import solo di tipi da emdash/plugin; nessun import runtime di emdash.

emdash-plugin build legge entrambi ed emette gli artefatti dist/ che un sito consuma.

L’esempio seguente mostra il layout dei file di un plugin completo:

my-plugin/
├── emdash-plugin.jsonc   # Identità + contratto di fiducia + profilo
├── src/
│   └── plugin.ts         # Hooks, routes — viene eseguito nel runtime sandbox
├── package.json
└── tsconfig.json

Configurare il package

  1. Crea la directory e un package.json. Il build è emdash-plugin build; non c’è invocazione tsdown da scrivere.

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

    "." è il descrittore generato che un sito importa; "./sandbox" è il file runtime costruito. emdash-plugin build genera entrambi.

  2. Aggiungi un tsconfig.json:

    {
    	"compilerOptions": {
    		"target": "ES2022",
    		"module": "preserve",
    		"moduleResolution": "bundler",
    		"strict": true,
    		"esModuleInterop": true,
    		"verbatimModuleSyntax": true,
    		"skipLibCheck": true,
    		"types": []
    	},
    	"include": ["src/**/*"],
    	"exclude": ["node_modules"]
    }

Scrivere il manifest

emdash-plugin.jsonc porta l’identità del plugin (slug), il suo contratto di fiducia (capabilities, allowedHosts, storage), campi profilo, e il publisher pin.

L’esempio seguente mostra un manifest completo per il 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"] } }
}

Note su questo manifest:

  • slug è un id sicuro per URL, non il nome del package npm. /^[a-z][a-z0-9_-]*$/, max 64 caratteri. È un singolo segmento di percorso nelle URL delle route del plugin (/_emdash/api/plugins/<slug>/...) e parte degli identificatori SQL generati per gli indici di storage, quindi @, /, cifre iniziali, e maiuscole falliscono. Abbina uno slug senza scope (plugin-hello) con un nome di package npm con scope.
  • storage dichiara le collezioni in anticipo. ctx.storage.events funziona al runtime solo perché events è dichiarato qui. Accedere a una collezione non dichiarata genera un errore.
  • version è omessa. Il build la legge da package.json così c’è un’unica fonte di verità. Vedi il riferimento del manifest.
  • Il contratto di fiducia è consenso. Modificare capabilities, allowedHosts, o storage successivamente richiede un bump di versione — i siti installati hanno acconsentito al vecchio contratto.

Scrivere il runtime

src/plugin.ts esporta per default un oggetto semplice annotato con satisfies SandboxedPlugin. emdash/plugin fornisce solo tipi, quindi un plugin sandboxed non ha dipendenza runtime su emdash.

L’esempio seguente registra ogni salvataggio di contenuto nello storage del plugin ed espone una route recent che restituisce gli ultimi dieci salvataggi:

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;

Note sul file runtime:

  • satisfies SandboxedPlugin tipizza tutto. Inferisce event dal nome dell’hook (con il tipo di evento canonico completo) e ctx come PluginContext, quindi gli handler non necessitano annotazioni di parametri. Una chiave di hook errata come "content:afterSav" è un errore di compilazione.
  • Gli handler di hooks prendono (event, ctx). La forma dell’evento dipende dal nome dell’hook; vedi la guida agli Hooks.
  • Gli handler di routes prendono (routeCtx, ctx) — due argomenti. routeCtx è { input, request, requestMeta? }; ctx è lo stesso PluginContext. Le routes sono raggiungibili a /_emdash/api/plugins/<slug>/<route-name>.
  • ctx.storage.events funziona perché events è dichiarato nel manifest.
  • ctx.kv è sempre disponibile — un key-value store per plugin con get, set, delete, list(prefix).

Registrare il plugin

Nel astro.config.mjs del sito, importa l’export di default del plugin e passalo. I plugin sandboxed vanno in sandboxed: []; i plugin in-process vanno in plugins: []. Un plugin sandboxed funziona in entrambi. L’esempio seguente 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 è il pezzo intercambiabile. L’esempio usa sandbox() da @emdash-cms/cloudflare, il runner che la maggior parte dei siti usa oggi. Se nessun runner è configurato (o il runner configurato non è disponibile sulla piattaforma corrente), i plugin sandboxed: [] vengono saltati all’avvio — sposta il plugin in plugins: [] per eseguirlo in-process.

Costruire ed eseguire

Dalla directory del plugin:

emdash-plugin validate   # controlla lo schema del manifest prima
emdash-plugin build      # emetti dist/

Per un ciclo di modifica, esegui emdash-plugin dev (ricostruisce al salvataggio, mantiene l’ultimo dist/ buono in caso di build fallito). Nel sito, installa o collega il plugin (pnpm add file:../plugin-hello o un link workspace) e avvia il dev server. Salva un contenuto nell’admin e dovresti vedere Content saved … nei log; GET /_emdash/api/plugins/plugin-hello/recent restituisce gli ultimi dieci eventi di salvataggio.

Cosa leggere successivamente