初めてのサンドボックスプラグイン

このページ

このガイドでは、最小限のサンドボックスプラグインをゼロから構築します。このプラグインはすべてのコンテンツ保存をログに記録し、単一のAPIルートを公開します。設定されたサンドボックスランナーによって提供される隔離されたランタイムで実行されます。同じコードは、サイト運営者がsandboxed: []からplugins: []に移動した場合(例えば、サンドボックスランナーのないプラットフォームで)、インプロセスでも実行されます。

サンドボックスとネイティブのどちらにするか決めていない場合は、まずプラグイン形式を選択するをお読みください。

2つの部分

サンドボックスプラグインは次のもので構成されます:

  1. emdash-plugin.jsonc — 手動編集されたマニフェスト:アイデンティティ、信頼契約(capabilities、hosts、storage)、およびプロファイルフィールド。コードなし。
  2. src/plugin.ts — ランタイム:フックとルート。emdash/pluginからの型のみのインポート;emdashのランタイムインポートなし。

emdash-plugin buildは両方を読み取り、サイトが消費するdist/アーティファクトを出力します。

次の例は、完全なプラグインのファイルレイアウトを示しています:

my-plugin/
├── emdash-plugin.jsonc   # アイデンティティ + 信頼契約 + プロファイル
├── src/
│   └── plugin.ts         # フック、ルート — サンドボックスランタイムで実行
├── package.json
└── tsconfig.json

パッケージのセットアップ

  1. ディレクトリとpackage.jsonを作成します。ビルドはemdash-plugin buildです;記述するtsdownの呼び出しはありません。

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

    "."はサイトがインポートする生成されたディスクリプタです;"./sandbox"はビルドされたランタイムファイルです。emdash-plugin buildは両方を生成します。

  2. tsconfig.jsonを追加します:

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

マニフェストの記述

emdash-plugin.jsoncは、プラグインのアイデンティティ(slug)、信頼契約(capabilitiesallowedHostsstorage)、プロファイルフィールド、およびパブリッシャーピンを保持します。

次の例は、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"] } }
}

このマニフェストに関する注意事項:

  • slugはURL安全なIDであり、npmパッケージ名ではありません。 /^[a-z][a-z0-9_-]*$/、最大64文字。プラグインルートURL(/_emdash/api/plugins/<slug>/...)の単一パスセグメントであり、ストレージインデックス用に生成されたSQL識別子の一部であるため、@/、先頭の数字、大文字はすべて失敗します。スコープなしのslugplugin-hello)をスコープ付きnpmパッケージ名とペアにします。
  • storageはコレクションを事前に宣言します。 ctx.storage.eventsは、eventsがここで宣言されているためにランタイムで機能します。宣言されていないコレクションへのアクセスはエラーをスローします。
  • versionは省略されています。 ビルドはpackage.jsonから読み取るため、真実の単一のソースがあります。マニフェストリファレンスを参照してください。
  • 信頼契約は同意です。 後でcapabilitiesallowedHosts、またはstorageを変更するには、バージョンバンプが必要です — インストールされたサイトは古い契約に同意しています。

ランタイムの記述

src/plugin.tsは、satisfies SandboxedPluginで注釈されたシンプルなオブジェクトをデフォルトでエクスポートします。emdash/pluginは型のみを提供するため、サンドボックスプラグインはemdashへのランタイム依存関係を持ちません。

次の例は、すべてのコンテンツ保存をプラグインストレージに記録し、最後の10回の保存を返すrecentルートを公開します:

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;

ランタイムファイルに関する注意事項:

  • satisfies SandboxedPluginはすべてを型付けします。 フック名からeventを推論し(完全な正規イベント型で)、ctxPluginContextとして推論するため、ハンドラはパラメータアノテーションを必要としません。"content:afterSav"のような誤ったフックキーはコンパイルエラーです。
  • フックハンドラは(event, ctx)を取ります。 イベントの形状はフック名に依存します;フックガイドを参照してください。
  • ルートハンドラは(routeCtx, ctx)を取ります — 2つの引数。routeCtx{ input, request, requestMeta? }です;ctxは同じPluginContextです。ルートは/_emdash/api/plugins/<slug>/<route-name>で到達可能です。
  • ctx.storage.eventsは、eventsがマニフェストで宣言されているため機能します。
  • ctx.kvは常に利用可能ですgetsetdeletelist(prefix)を持つプラグインごとのキーバリューストア。

プラグインの登録

サイトのastro.config.mjsで、プラグインのデフォルトエクスポートをインポートして渡します。サンドボックスプラグインはsandboxed: []に入ります;インプロセスプラグインはplugins: []に入ります。サンドボックスプラグインは両方で機能します。次の例では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は交換可能な部分です。この例では、今日ほとんどのサイトが使用するランナーである@emdash-cms/cloudflaresandbox()を使用しています。ランナーが設定されていない場合(または設定されたランナーが現在のプラットフォームで利用できない場合)、sandboxed: []プラグインは起動時にスキップされます — プラグインをplugins: []に移動してインプロセスで実行します。

ビルドと実行

プラグインディレクトリから:

emdash-plugin validate   # まずマニフェストのスキーマチェック
emdash-plugin build      # dist/を出力

編集ループの場合は、emdash-plugin devを実行します(保存時に再構築し、ビルド失敗時には最後の良好なdist/を保持します)。サイトで、プラグインをインストールまたはリンクし(pnpm add file:../plugin-helloまたはワークスペースリンク)、開発サーバーを起動します。管理画面でコンテンツを保存すると、ログにContent saved …が表示されます;GET /_emdash/api/plugins/plugin-hello/recentは最後の10個の保存イベントを返します。

次に読むべきもの