Ihr erstes Sandboxed Plugin

Auf dieser Seite

Diese Anleitung erstellt ein minimales Sandboxed Plugin von Grund auf. Das Plugin protokolliert jedes Speichern von Inhalten und stellt eine einzige API-Route bereit. Es läuft in einer isolierten Laufzeitumgebung, die vom konfigurierten Sandbox-Runner bereitgestellt wird. Derselbe Code läuft auch im Prozess, wenn ein Site-Betreiber ihn von sandboxed: [] in plugins: [] verschiebt, beispielsweise auf einer Plattform ohne Sandbox-Runner.

Falls Sie sich noch nicht zwischen Sandboxed und Native entschieden haben, lesen Sie zuerst Ein Plugin-Format wählen.

Zwei Komponenten

Ein Sandboxed Plugin besteht aus:

  1. emdash-plugin.jsonc — ein manuell bearbeitetes Manifest: Identität, der Vertrauensvertrag (Capabilities, Hosts, Storage) und Profilfelder. Kein Code.
  2. src/plugin.ts — die Laufzeitumgebung: Hooks und Routes. Nur Type-Importe aus emdash/plugin; kein Laufzeit-Import von emdash.

emdash-plugin build liest beide und erzeugt die dist/-Artefakte, die eine Site konsumiert.

Das folgende Beispiel zeigt das Dateilayout eines vollständigen Plugins:

my-plugin/
├── emdash-plugin.jsonc   # Identität + Vertrauensvertrag + Profil
├── src/
│   └── plugin.ts         # Hooks, Routes — läuft in der Sandbox-Laufzeit
├── package.json
└── tsconfig.json

Das Paket einrichten

  1. Erstellen Sie das Verzeichnis und eine package.json. Der Build ist emdash-plugin build; es gibt keinen tsdown-Aufruf zu schreiben.

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

    "." ist der generierte Deskriptor, den eine Site importiert; "./sandbox" ist die gebaute Laufzeitdatei. emdash-plugin build generiert beide.

  2. Fügen Sie eine tsconfig.json hinzu:

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

Das Manifest schreiben

emdash-plugin.jsonc trägt die Identität des Plugins (slug), seinen Vertrauensvertrag (capabilities, allowedHosts, storage), Profilfelder und den Publisher Pin.

Das folgende Beispiel zeigt ein vollständiges Manifest für das Hello-Plugin:

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

Hinweise zu diesem Manifest:

  • slug ist eine URL-sichere ID, nicht der npm-Paketname. /^[a-z][a-z0-9_-]*$/, max 64 Zeichen. Es ist ein einzelnes Pfadsegment in Plugin-Route-URLs (/_emdash/api/plugins/<slug>/...) und Teil generierter SQL-Identifikatoren für Speicherindizes, daher schlagen @, /, führende Ziffern und Großbuchstaben fehl. Kombinieren Sie einen Unscoped-slug (plugin-hello) mit einem Scoped-npm-Paketnamen.
  • storage deklariert Collections im Voraus. ctx.storage.events funktioniert zur Laufzeit nur, weil events hier deklariert ist. Der Zugriff auf eine nicht deklarierte Collection wirft einen Fehler.
  • version wird weggelassen. Der Build liest sie aus package.json, sodass es eine einzige Quelle der Wahrheit gibt. Siehe die Manifest-Referenz.
  • Der Vertrauensvertrag ist Zustimmung. Die Änderung von capabilities, allowedHosts oder storage erfordert später einen Version-Bump — installierte Sites haben dem alten Vertrag zugestimmt.

Die Laufzeitumgebung schreiben

src/plugin.ts exportiert standardmäßig ein einfaches Objekt, das mit satisfies SandboxedPlugin annotiert ist. emdash/plugin bietet nur Typen, sodass ein Sandboxed Plugin keine Laufzeitabhängigkeit von emdash hat.

Das folgende Beispiel protokolliert jedes Speichern von Inhalten im Plugin-Speicher und stellt eine recent-Route bereit, die die letzten zehn Speichervorgänge zurückgibt:

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;

Hinweise zur Laufzeitdatei:

  • satisfies SandboxedPlugin typisiert alles. Es leitet event vom Hook-Namen ab (mit dem vollständigen kanonischen Event-Typ) und ctx als PluginContext, sodass Handler keine Parameter-Annotationen benötigen. Ein vertippter Hook-Schlüssel wie "content:afterSav" ist ein Compiler-Fehler.
  • Hook-Handler nehmen (event, ctx). Die Event-Form hängt vom Hook-Namen ab; siehe die Hooks-Anleitung.
  • Route-Handler nehmen (routeCtx, ctx) — zwei Argumente. routeCtx ist { input, request, requestMeta? }; ctx ist derselbe PluginContext. Routes sind erreichbar unter /_emdash/api/plugins/<slug>/<route-name>.
  • ctx.storage.events funktioniert, weil events im Manifest deklariert ist.
  • ctx.kv ist immer verfügbar — ein plugin-spezifischer Key-Value-Store mit get, set, delete, list(prefix).

Das Plugin registrieren

In der astro.config.mjs der Site importieren Sie den Standard-Export des Plugins und übergeben ihn. Sandboxed Plugins kommen in sandboxed: []; In-Process-Plugins in plugins: []. Ein Sandboxed Plugin funktioniert in beiden. Das folgende Beispiel verwendet 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 ist das austauschbare Teil. Das Beispiel verwendet sandbox() aus @emdash-cms/cloudflare, den Runner, den die meisten Sites heute verwenden. Wenn kein Runner konfiguriert ist (oder der konfigurierte Runner auf der aktuellen Plattform nicht verfügbar ist), werden sandboxed: []-Plugins beim Start übersprungen — verschieben Sie das Plugin in plugins: [], um es im Prozess auszuführen.

Erstellen und ausführen

Vom Plugin-Verzeichnis aus:

emdash-plugin validate   # Schema-Prüfung des Manifests zuerst
emdash-plugin build      # dist/ erzeugen

Für eine Bearbeitungsschleife führen Sie emdash-plugin dev aus (baut bei Speicherung neu, behält das letzte gute dist/ bei einem fehlgeschlagenen Build). Installieren oder verknüpfen Sie in der Site das Plugin (pnpm add file:../plugin-hello oder einen Workspace-Link) und starten Sie den Dev-Server. Speichern Sie einen Inhalt im Admin und Sie sollten Content saved … in den Logs sehen; GET /_emdash/api/plugins/plugin-hello/recent gibt die letzten zehn Speicher-Events zurück.

Was Sie als Nächstes lesen sollten