Dieser Leitfaden führt Sie durch die Erstellung eines minimalen Sandbox-Plugins von Grund auf — ein Plugin, das jedes Speichern von Inhalten protokolliert und eine einzelne API-Route bereitstellt. Am Ende haben Sie ein Plugin, das über den konfigurierten Sandbox-Runner in einer isolierten Laufzeitumgebung ausgeführt wird. Derselbe Plugin-Code kann auch prozessintern ausgeführt werden, wenn der Site-Betreiber es von sandboxed: [] nach plugins: [] verschiebt — zum Beispiel auf Plattformen ohne verfügbaren Sandbox-Runner.
Wenn Sie noch nicht entschieden haben, ob Sie ein Sandbox- oder natives Plugin möchten, lesen Sie zuerst Ein Plugin-Format wählen.
Zwei Dateien
Jedes Sandbox-Plugin besteht aus zwei Teilen:
- Ein Deskriptor — ein kleines Objekt, das das Plugin beschreibt (id, Version, Fähigkeiten, Speicher, wo der Laufzeit-Einstiegspunkt zu finden ist). Wird zur Build-Zeit von
astro.config.mjsimportiert. - Ein Sandbox-Einstiegspunkt — der Laufzeit-Code: Hooks, Routen, Speicherzugriff. Wird zur Anfragezeit in die Sandbox-Laufzeit geladen.
Die beiden Dateien befinden sich im selben Paket und laufen in völlig unterschiedlichen Umgebungen. Der Deskriptor sieht niemals den Laufzeit-Kontext; der Einstiegspunkt sieht niemals astro.config.mjs.
my-plugin/
├── src/
│ ├── index.ts # Deskriptor — läuft zur Build-Zeit in Vite
│ └── sandbox-entry.ts # Hooks, Routen, Speicher — läuft in der Sandbox-Laufzeit
├── package.json
└── tsconfig.json
Das Paket einrichten
-
Erstellen Sie ein neues Verzeichnis und initialisieren Sie es als TypeScript-ES-Modul-Paket.
{ "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/sandbox-entry.mjs" }, "files": ["dist"], "scripts": { "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean" }, "peerDependencies": { "emdash": "*" }, "devDependencies": { "emdash": "*", "tsdown": "^0.6.0", "typescript": "^5.5.0" } }Der
"./sandbox"-Export ist das, worauf derentrypointdes Deskriptors verweist. Der Bundler baut beide Dateien nachdist/. -
Fügen Sie eine
tsconfig.jsonhinzu:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Den Deskriptor schreiben
Der Deskriptor ist eine Factory-Funktion, die einen PluginDescriptor zurückgibt. Er läuft zur Build-Zeit in Vite, was bedeutet, dass er nebenwirkungsfrei sein muss und keine Laufzeit-APIs verwenden kann (fetch, die Datenbank, Umgebungsvariablen — nichts davon existiert noch).
import type { PluginDescriptor } from "emdash";
export function helloPlugin(): PluginDescriptor {
return {
id: "plugin-hello",
version: "0.1.0",
format: "standard",
entrypoint: "@my-org/plugin-hello/sandbox",
capabilities: [],
storage: {
events: { indexes: ["timestamp"] },
},
};
}
Einige wichtige Details:
format: "standard"ist erforderlich. Ohne es behandelt EmDash das Paket als natives Plugin und sucht nach einer anderen Form. Dasformat-Feld ist standardmäßig"native".entrypointist ein Modul-Spezifizierer, kein Dateipfad. Verwenden Sie denselben String, den Sie animportübergeben würden — normalerweise"<package-name>/sandbox". Der Paketname kann gescoped sein (@my-org/plugin-hello); die Plugin-idkann es nicht.idist ein URL-sicherer Slug, nicht der npm-Paketname. Es muss/^[a-z][a-z0-9_-]*$/entsprechen — mit einem Kleinbuchstaben beginnen, gefolgt von Buchstaben, Ziffern, Bindestrichen oder Unterstrichen. Die id wird sowohl als einzelnes Pfadsegment in Plugin-Routen-URLs (/_emdash/api/plugins/<id>/...) als auch als Teil von generierten SQL-Identifikatoren für Plugin-Speicherindizes verwendet, daher schlagen@,/, führende Ziffern und Großbuchstaben alle zur Laufzeit fehl. Paaren Sie eine ungescopedidwieplugin-hellomit einem gescoped npm-Paketnamen inentrypoint.- Fähigkeiten, allowedHosts und Speicher befinden sich im Deskriptor. Der Sandbox-Einstiegspunkt deklariert sie nicht — er kann nur verwenden, was der Deskriptor erlaubt.
- Fügen Sie hier keine Laufzeit-Logik ein. Kein Top-Level-
await, kein Modul-Level-fetch, kein Lesen von Dateien. Der Deskriptor sind Metadaten.
Den Sandbox-Einstiegspunkt schreiben
Die Laufzeit-Seite. Diese Datei wird zur Anfragezeit in die Sandbox-Laufzeit geladen, ohne Zugriff auf irgendetwas außer dem, was ctx bereitstellt.
import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";
interface ContentSaveEvent {
collection: string;
content: { id: string };
isNew: boolean;
}
export default definePlugin({
hooks: {
"content:afterSave": {
handler: async (event: ContentSaveEvent, ctx: PluginContext) => {
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: PluginContext) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
});
Wissenswerte Dinge:
definePlugin()im Sandbox-Einstiegspunkt nimmt nur{ hooks, routes }. Keineid, keineversion, keinecapabilities— diese kommen vom Deskriptor. EmDash wirft zur Build-Zeit einen Fehler, wenn Sie versuchen, sie hier zu übergeben.- Hook-Handler nehmen
(event, ctx). Die Event-Form hängt vom Hook-Namen ab; siehe die Hooks-Referenz. - Routen-Handler nehmen
(routeCtx, ctx)— zwei Argumente.routeCtxhat{ input, request, requestMeta };ctxist derselbePluginContext, den Sie in Hooks erhalten. Routen sind unter/_emdash/api/plugins/<plugin-id>/<route-name>erreichbar. ctx.storage.eventsfunktioniert, weileventsim Deskriptor deklariert wurde. Der Zugriff auf eine nicht deklarierte Sammlung wirft einen Fehler.ctx.kvist immer verfügbar — ein pro-Plugin-Key-Value-Speicher mitget,set,deleteundlist(prefix).
Das Plugin registrieren
Importieren Sie in der astro.config.mjs Ihrer Site die Deskriptor-Factory und übergeben Sie sie an die EmDash-Integration. Sandbox-Plugins gehen in sandboxed: []; prozessinterne Plugins gehen in plugins: []. Ein Plugin im Standardformat funktioniert in beiden — beginnen Sie mit sandboxed.
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import { helloPlugin } from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [helloPlugin()],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner ist das austauschbare Teil. Das obige Beispiel verwendet sandbox() von @emdash-cms/cloudflare, dem Sandbox-Runner, den die meisten Sites heute verwenden. Runner für andere Plattformen sind in Entwicklung. Wenn kein Runner konfiguriert ist (oder der konfigurierte Runner als nicht verfügbar auf der aktuellen Plattform meldet), werden sandboxed: []-Plugins beim Start übersprungen — um dasselbe Plugin prozessintern auszuführen, verschieben Sie es von sandboxed: [] nach plugins: [].
Bauen und ausführen
Aus dem Plugin-Verzeichnis:
pnpm build
Verknüpfen oder installieren Sie im Site-Verzeichnis das Plugin (pnpm add @my-org/plugin-hello oder ein Workspace-Link) und starten Sie dann den Dev-Server. Sie sollten [hello] Content saved … in den Logs sehen, wenn Sie das nächste Mal einen Inhalt im Admin speichern, und GET /_emdash/api/plugins/plugin-hello/recent sollte die letzten zehn Speicher-Events zurückgeben.
Was als Nächstes zu lesen ist
- Hooks — das vollständige Set von Events, auf die Sie reagieren können
- API-Routen — Eingabevalidierung, öffentliche Routen, Fehlerbehandlung
- Speicher und KV — Abfrageoptionen, Indizes, Batch-Operationen
- Fähigkeiten und Sicherheit — Inhaltszugriff, Netzwerkanfragen, Host-Whitelists
- Bundling und Veröffentlichung — wenn Sie bereit sind, auf dem Marktplatz zu versenden