はじめてのサンドボックス型プラグイン
このページで分かること
emdash-pluginCLIでプラグインの雛形を作り、マニフェストに権限(content:read)とストレージを宣言する手順- フック(
content:afterSave)とルート(health)の追加、生成されたテストの書き換え、検証とビルド - ビルドしたプラグインをサイトの
astro.config.mjsに登録し、動作を確かめる方法
このページの目次
このチュートリアルでは、コンテンツの保存イベントを記録し、小さなヘルスチェック用のルートを公開するサンドボックス型プラグインを作ります。プラグインCLIでパッケージの雛形を作り、フックとルートを1つずつ追加し、プラグインをEmDashのサイトに登録して、両方のハンドラーが動くことを確かめます。
プラグインの形式をまだ選んでいない場合は、先にプラグイン形式の選び方を読みます。
前提条件
次のものが必要です。
- Node.jsとpnpm
- サンドボックスランナーを設定したEmDashのサイト
- マニフェストのpublisherフィールドに使う、AtmosphereアカウントのハンドルまたはDID
プラグインの雛形を作る
-
新しいプロジェクトを置くディレクトリで、プラグインの作成ツールを実行します。
pnpm dlx @emdash-cms/plugin-cli init save-logコマンドは、公開者(publisher)、作成者(author)、セキュリティの連絡先、ソースリポジトリを尋ねたあと、プロジェクトの概要を表示してから、次の構成を作成します。
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 -
生成されたパッケージの依存パッケージをインストールします。
cd save-log pnpm install
やさしい解説
公式の対応表では、WordPressのプラグインにあたるのがEmDashのプラグインです。サンドボックス型プラグインは、emdash-plugin CLIが作る雛形から始めます。雛形には、プラグインの宣言を書く emdash-plugin.jsonc(マニフェスト)、処理を書く src/plugin.ts、テストの tests/plugin.test.ts が含まれます。このチュートリアルでは、この3つのファイルを順に書き換えます。
プラグインのアクセスとストレージを定義する
emdash-plugin.jsonc には、プラグインの識別情報、レジストリの情報、信頼に関する取り決めが入っています。content:afterSave は保存されたコンテンツをプラグインに渡すため、content:read の権限(Capability)を追加します。また、フックが保存ごとの記録を検索できる形で残せるように、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 にアクセスすると例外が発生します。残りのフィールドと検証ルールは、マニフェストのリファレンスで説明しています。
やさしい解説
サンドボックス型プラグインは、マニフェストに書いた権限とストレージしか使えません。ここでは、保存されたコンテンツを受け取るために content:read を、記録を残すために 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 ルートは公開(public)で読み取り専用のため、管理画面のセッションなしで確認できます。公開ルートはインターネットからアクセスできます。実際のデータや変更操作を公開する前に、APIルートで認証とブラウザのオリジンのルールを確認してください。
生成されたテストを更新する
雛形のテストは、元の hello ルートを、Worker Loaderのトランスポートホストを通じて呼び出します。これを 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 を通じてルートを呼び出します。フック、コンテンツのフィクスチャー、ストレージのアサーション、ローカルの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のサイトにインストールします。次のコマンドはサイトのディレクトリで実行します。2つのプロジェクトが同じ階層にない場合は、相対パスを調整します。
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やその他の対応する構成を使っている場合は、すでに設定されているランナーをそのまま使います。
プラグインを動かす
2つの開発用のプロセスを起動します。
- プラグインのディレクトリで
pnpm devを実行します。ソースまたはマニフェストが変わると、CLIがプラグインをビルドし直します。 - サイトのディレクトリで、サイトの開発用コマンドを実行します。
サイトで次のルートを開きます。
http://localhost:4321/_emdash/api/plugins/save-log/health
レスポンスには、標準のAPIのエンベロープと、プラグインが返した値が入っています。
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
EmDashの管理画面でエントリーを保存します。サイトのログに Content save recorded が出力され、フックがプラグインの events コレクションに1件書き込みます。
やさしい解説
ここで作ったのは、保存のたびに記録を残すフックと、動作確認用の health ルートを持つプラグインです。確認は2つです。1つ目は、ブラウザで health ルートを開き、"ok": true が返ることです。2つ目は、管理画面でエントリーを保存したときに、サイトのログに Content save recorded が出ることです。ランナーが使えない場合、EmDashは sandboxed のプラグインを読み込まないため、どちらも出ないときはサンドボックスランナーの設定を確認します。