初めての sandboxed プラグイン

このページ

このチュートリアルでは、コンテンツ保存イベントを記録し、小さなヘルスルートを公開する sandboxed プラグインを作成します。プラグイン CLI でパッケージをスキャフォールドし、フックとルートを 1 つずつ追加し、EmDash サイトに登録して、両方のハンドラが動くことを確認します。

プラグイン形式をまだ選んでいない場合は、先に プラグイン形式の選択 を読んでください。

前提条件

次が必要です。

プラグインをスキャフォールドする

  1. 新しいプロジェクトを置くディレクトリからプラグインスキャフォールダを実行します。

    pnpm dlx @emdash-cms/plugin-cli init save-log

    コマンドは publisher、著者、セキュリティ連絡先、ソースリポジトリを尋ね、この構造を作成する前にプロジェクト概要を表示します。

    save-log/
    ├── .agents/
    │   └── skills -> ../skills
    ├── .claude/
    │   ├── CLAUDE.md -> ../AGENTS.md
    │   └── skills -> ../skills
    ├── AGENTS.md
    ├── emdash-plugin.jsonc
    ├── package.json
    ├── pnpm-workspace.yaml
    ├── README.md
    ├── skills/
    │   └── creating-plugins/SKILL.md
    ├── src/
    │   └── plugin.ts
    ├── tests/
    │   └── plugin.test.ts
    ├── tsconfig.json
    ├── vitest.config.ts
    └── .gitignore
  2. 生成されたパッケージの依存関係をインストールします。

    cd save-log
    pnpm install

プラグインのアクセスとストレージを定義する

emdash-plugin.jsonc にはプラグインの識別情報、レジストリ情報、信頼契約が含まれます。content:afterSave が保存済みコンテンツをプラグインに公開するため、content:read ケイパビリティを追加してください。フックが各保存の照会可能な記録を保てるよう、events ストレージコレクションを宣言します。

次のマニフェストにはこのチュートリアルで使うフィールドが含まれます。スキャフォールダが生成した publisher、author、security の値はそのままにしてください。

{
	"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",

	"slug": "save-log",
	"publisher": "did:plc:abc123def456",

	"license": "MIT",
	"author": { "name": "Jane Doe", "url": "https://example.com" },
	"security": { "email": "security@example.com" },
	"description": "Records content-save events.",

	"capabilities": ["content:read"],
	"allowedHosts": [],
	"storage": {
		"events": { "indexes": ["savedAt"] },
	},
}

content:read 宣言は、フックが保存済みコンテンツを受け取ることをサイト運用者に伝え、このプラグイン形式がプロセス内で動くときに必要です。コレクションがないと ctx.storage.events へのアクセスは例外を投げます。マニフェストリファレンス が残りのフィールドと検証ルールを説明します。

フックとルートを追加する

生成された src/plugin.ts を次のランタイム定義に置き換えます。

import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	hooks: {
		"content:afterSave": {
			handler: async (event, ctx) => {
				const savedAt = new Date().toISOString();
				const contentId = String(event.content.id);
				await ctx.storage.events.put(`${savedAt}:${contentId}`, {
					savedAt,
					collection: event.collection,
					contentId,
				});

				ctx.log.info("Content save recorded", {
					collection: event.collection,
					contentId,
				});
			},
		},
	},

	routes: {
		health: {
			public: true,
			handler: async (_routeCtx, ctx) => {
				return { ok: true, plugin: ctx.plugin.id };
			},
		},
	},
};

export default plugin;

src/plugin.ts は定義を SandboxedPlugin 型の定数に代入し、デフォルトエクスポートします。注釈は、EmDash ランタイムをバンドルに足したりパッケージマネージャ固有の宣言パスを作ったりせずに、フックとルートにパラメータ型を与えます。

フックハンドラは (event, ctx) を受け取ります。ルートハンドラは (routeCtx, ctx) を受け取ります。health ルートは公開かつ読み取り専用なので、管理セッションなしで確認できます。公開ルートはインターネットに面します。実データやミューテーションを公開する前に、API ルート が認証とブラウザオリジンのルールを説明します。

生成されたテストを更新する

スキャフォールドされたテストは Worker Loader トランスポートホスト経由で元の hello ルートを呼び出します。health ルートのテストに置き換えてください。

import { afterEach, describe, expect, it } from "vitest";

import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";

let host: PluginTestHost | undefined;

afterEach(async () => {
	await host?.dispose();
	host = undefined;
});

describe("health route", () => {
	it("identifies the running plugin", async () => {
		host = await createPluginTestHost();
		const result = await host.invokeRoute("health");
		expect(result).toEqual({ ok: true, plugin: "save-log" });
	});
});

テストはプラグインをビルドし、Worker Loader と PluginBridge 経由でルートを呼び出します。sandboxed プラグインのテストガイド はフック、コンテンツフィクスチャ、ストレージアサーション、ローカル workerd テストの限界を扱います。

検証とビルド

生成されたテストを実行し、マニフェストを検証し、npm 成果物をビルドします。

pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build

ビルドは次を作成します。

  • フックとルートのコードを含む dist/plugin.mjs
  • ランタイムマニフェストと検出されたフック・ルート名を含む dist/manifest.json
  • サイトがインポートするデフォルトエクスポートのディスクリプタ dist/index.mjs

dist/ は生成出力です。プラグインビルドが再作成するため、スキャフォールドは Git から除外します。

プラグインを登録する

ローカルパッケージを EmDash サイトにインストールします。サイトのディレクトリからこのコマンドを実行し、プロジェクトが兄弟でない場合は相対パスを調整してください。

pnpm add file:../save-log

生成されたデフォルトエクスポートを astro.config.mjs にインポートし、sandboxed に追加します。

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";

export default defineConfig({
	integrations: [
		emdash({
			sandboxed: [saveLog],
			sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
		}),
	],
});

この例は Node.js の workerd ランナーを使います。サイトが Cloudflare Workers や他のサポートされる構成を使う場合は、既に構成済みのランナーをそのままにしてください。

プラグインを実行する

両方の開発プロセスを起動します。

  1. プラグインディレクトリで pnpm dev を実行します。ソースまたはマニフェストが変わると CLI がプラグインを再ビルドします。
  2. サイトディレクトリでサイトの開発コマンドを実行します。

サイトで次のルートを開きます。

http://localhost:4321/_emdash/api/plugins/save-log/health

応答には標準の API エンベロープとプラグインが返した値が含まれます。

{
	"success": true,
	"data": { "ok": true, "plugin": "save-log" },
}

EmDash 管理画面でエントリーを保存します。サイトログに Content save recorded があり、フックはプラグインの events コレクションに 1 項目を書き込みます。

続けて構築する

  • フック はフックイベント、ケイパビリティ、順序、エラーを説明します。
  • API ルート は検証、権限、公開ルート、MCP 公開を扱います。
  • Block Kit はブラウザ JavaScript を出荷せずに管理ページを追加します。
  • 設定 はサイト固有のプラグイン構成を保存します。
  • ストレージ はインデックス付きクエリとページネーションを扱います。
  • テスト は直接サンドボックス転送テストとランタイム支援のホストアクションテストを扱います。
  • バンドルと公開 はプラグインをレジストリに公開します。