最初のネイティブプラグイン

このページ

このガイドでは、ネイティブプラグインをゼロから構築する方法を説明します。ネイティブプラグインは、React管理ページ、Portable Textコンポーネント、ページフラグメントを含む、ランタイムへのフルアクセスを持つAstroサイトと同じプロセスで実行されます。

ネイティブプラグインかサンドボックスプラグインかまだ決めていない場合は、まずプラグインフォーマットの選択をお読みください。ネイティブは、React管理ページ、Portable Textレンダリングコンポーネント、またはページフラグメントが必要なプラグイン用のフォーマットです。

2つのパーツ、1つまたは2つのファイルで

サンドボックスプラグインと同様に、ネイティブプラグインは2つのパーツを提供します:

  1. ディスクリプタファクトリformat: "native" とadmin関連のエントリーポイントを持つ PluginDescriptor を返します。ビルド時に astro.config.mjs からインポートされます。
  2. createPlugin(options) 関数 — ランタイム側。definePlugin({ id, version, capabilities, hooks, routes, admin }) の結果を返します。

サンドボックスプラグインと異なり、両方のパーツは異なる環境で実行されないため同じファイルに配置できます — プラグイン全体がインプロセスで実行されます。パッケージの "." エクスポートは、ディスクリプタファクトリと createPlugin(または default)関数の両方をエクスポートするファイルを指します:

my-native-plugin/
├── src/
│   ├── index.ts          # ディスクリプタファクトリ + createPlugin
│   ├── admin.tsx         # React管理コンポーネント(オプション)
│   └── astro/            # PTブロックレンダリング用Astroコンポーネント(オプション)
│       └── index.ts
├── package.json
└── tsconfig.json

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

以下の package.json は、ネイティブプラグインに必要なエントリーポイントとピア依存関係を宣言します:

{
	"name": "@my-org/plugin-analytics",
	"version": "0.1.0",
	"type": "module",
	"main": "dist/index.js",
	"exports": {
		".": {
			"types": "./dist/index.d.ts",
			"import": "./dist/index.js"
		},
		"./admin": {
			"types": "./dist/admin.d.ts",
			"import": "./dist/admin.js"
		}
	},
	"files": ["dist"],
	"peerDependencies": {
		"emdash": "*",
		"react": "^18.0.0"
	}
}

ホストサイトが実際のバージョンを提供し、重複を出荷しないように、emdashreact をピア依存関係として保持してください。

ディスクリプタとランタイムの記述

以下の src/index.ts は、ディスクリプタファクトリと createPlugin ランタイムを1つのファイルで定義します:

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

export interface AnalyticsOptions {
	enabled?: boolean;
	maxEvents?: number;
}

export function analyticsPlugin(options: AnalyticsOptions = {}): PluginDescriptor {
	return {
		id: "analytics",
		version: "0.1.0",
		format: "native",
		entrypoint: "@my-org/plugin-analytics",
		options,
		adminEntry: "@my-org/plugin-analytics/admin",
		adminPages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
		adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
	};
}

export function createPlugin(options: AnalyticsOptions = {}) {
	const maxEvents = options.maxEvents ?? 100;

	return definePlugin({
		id: "analytics",
		version: "0.1.0",

		capabilities: ["network:request"],
		allowedHosts: ["api.analytics.example.com"],

		storage: {
			events: { indexes: ["type", "createdAt"] },
		},

		admin: {
			entry: "@my-org/plugin-analytics/admin",
			settingsSchema: {
				trackingId: { type: "string", label: "Tracking ID" },
				enabled: { type: "boolean", label: "Enabled", default: options.enabled ?? true },
			},
			pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
			widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
		},

		hooks: {
			"plugin:install": async (_event, ctx) => {
				ctx.log.info("Analytics plugin installed", { maxEvents });
			},

			"content:afterSave": async (event, ctx) => {
				const enabled = await ctx.kv.get<boolean>("settings:enabled");
				if (enabled === false) return;

				await ctx.storage.events.put(`evt_${Date.now()}`, {
					type: "content:save",
					contentId: event.content.id,
					createdAt: new Date().toISOString(),
				});
			},
		},

		routes: {
			stats: {
				handler: async (ctx) => {
					const today = new Date().toISOString().split("T")[0];
					const count = await ctx.storage.events.count({
						createdAt: { gte: today },
					});
					return { today: count };
				},
			},
		},
	});
}

export default createPlugin;

この設定の重要な詳細:

  • format: "native" は必須です。 "native" はデフォルト値でもありますが、すべてのディスクリプタに明示的に記載することでフォーマットを簡単に識別できます。
  • entrypoint はパッケージのメインエクスポートです。 EmDashはランタイム時にこれをインポートし、デフォルトエクスポートを呼び出して解決済みプラグインを構築します。
  • options はディスクリプタ → createPlugin に流れます。 ユーザーがプラグイン登録時に渡すもの(analyticsPlugin({ enabled: false }))はすべてディスクリプタに保持され、createPlugin に転送されます。サンドボックスプラグインにはこのサーフェスがありません — 代わりにKVから設定を読み取ります。
  • idversioncapabilities は2回出現します。 ディスクリプタに1回、definePlugin() に1回。一致する必要があります。ディスクリプタのコピーはビルド時に astro.config.mjs が見るものです。definePlugin() のコピーはリクエスト時に実行されるものです。
  • ネイティブルートハンドラは単一の引数を取ります(ctx: RouteContext) で、ctx.inputctx.requestctx.requestMeta が通常の PluginContext プロパティとマージされます。これは標準フォーマットの2引数形式と逆です。完全なサーフェスについてはAPIルートを参照してください(それ以外はすべて同一です)。

プラグインIDルール

id フィールドは /^[a-z][a-z0-9_-]*$/ に一致する必要があります — 小文字で始まり、その後に文字、数字、ハイフン、またはアンダースコアが続きます。IDはプラグインルートURLの単一パスセグメントとして使用され、プラグインストレージインデックスの生成されたSQL識別子の一部として使用されるため、このパターン外のものはランタイムで失敗します。以下の値は、どのIDが受け入れられるかを示します:

// 有効
"seo";
"audit-log";
"audit_log";
"plugin-forms";

// 無効
"@my-org/plugin-forms";  // スコープ付き形式はランタイムで許可されません
"MyPlugin";              // 大文字不可
"42-plugin";             // 数字で始めることはできません
"my.plugin";             // ドット不可

スコープなしの identrypoint のスコープ付きnpmパッケージ名を組み合わせてください — パッケージ名とプラグインIDは別の関心事です。

バージョンフォーマット

セマンティックバージョニングを使用してください。以下の値は、どのバージョン文字列が受け入れられるかを示します:

version: "1.0.0";       // 有効
version: "1.2.3-beta";  // 有効(プレリリース)
version: "1.0";         // 無効(パッチが欠落)

プラグインの登録

サイトの astro.config.mjs で、ディスクリプタファクトリをインポートし、plugins: [] 配列に渡します — ネイティブプラグインは常にインプロセスで実行され、sandboxed: [] には配置しません:

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { analyticsPlugin } from "@my-org/plugin-analytics";

export default defineConfig({
	integrations: [
		emdash({
			plugins: [
				analyticsPlugin({ enabled: true, maxEvents: 500 }),
			],
		}),
	],
});

設定UI

ネイティブプラグインは、最もシンプルな方法として、自動生成される設定フォーム用に admin.settingsSchema を使用できます:

admin: {
	settingsSchema: {
		apiKey: { type: "secret", label: "API Key" },
		enabled: { type: "boolean", label: "Enabled", default: true },
		maxItems: { type: "number", label: "Max items", min: 1, max: 1000, default: 100 },
	},
},

フィールドタイプ:stringnumberbooleanselectsecreturlemail。各タイプは labeldescriptiondefault に加え、min/max/options などのタイプ固有の拡張を受け入れます。設定はサンドボックスプラグインが使用するのと同じプラグインごとのKVストアに永続化されます — ctx.kv.get<T>("settings:<key>") でどこからでも読み取れます。

生成されたフォームは Plugins のプラグインカードの歯車アイコンの後ろに表示されます(管理者のみ — プラグイン設定の編集には plugins:manage 権限が必要です)。シークレットフィールドは書き込み専用です:管理者は保存された値を見ることはなく、設定されているかどうかのみ確認できます。

settingsSchema が提供するよりもリッチな設定UIには、カスタムReactページを提供してください — React管理ページとウィジェットを参照してください。

完全な例 — 監査ログプラグイン

以下のプラグインは、すべてのコンテンツの作成、更新、削除をインデックス付きストレージに記録し、最近のアクティビティルートを公開します:

import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";

interface AuditEntry {
	timestamp: string;
	action: "create" | "update" | "delete";
	collection: string;
	resourceId: string;
	userId?: string;
}

export function auditLogPlugin(): PluginDescriptor {
	return {
		id: "audit-log",
		version: "0.1.0",
		format: "native",
		entrypoint: "@emdash-cms/plugin-audit-log",
	};
}

export function createPlugin() {
	return definePlugin({
		id: "audit-log",
		version: "0.1.0",

		storage: {
			entries: {
				indexes: [
					"timestamp",
					"action",
					"collection",
					["collection", "timestamp"],
					["action", "timestamp"],
				],
			},
		},

		admin: {
			settingsSchema: {
				retentionDays: {
					type: "number",
					label: "Retention (days)",
					description: "Days to keep entries. 0 = forever.",
					default: 90,
					min: 0,
					max: 365,
				},
			},
			pages: [{ path: "/history", label: "Audit History", icon: "history" }],
			widgets: [{ id: "recent-activity", title: "Recent Activity", size: "half" }],
		},

		hooks: {
			"content:afterSave": {
				priority: 200,
				handler: async (event, ctx) => {
					const entry: AuditEntry = {
						timestamp: new Date().toISOString(),
						action: event.isNew ? "create" : "update",
						collection: event.collection,
						resourceId: event.content.id as string,
					};
					await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, entry);
				},
			},

			"content:afterDelete": {
				priority: 200,
				handler: async (event, ctx) => {
					await ctx.storage.entries.put(`${Date.now()}-${event.id}`, {
						timestamp: new Date().toISOString(),
						action: "delete",
						collection: event.collection,
						resourceId: event.id,
					});
				},
			},
		},

		routes: {
			recent: {
				handler: async (ctx) => {
					const result = await ctx.storage.entries.query({
						orderBy: { timestamp: "desc" },
						limit: 10,
					});
					return {
						entries: result.items.map((item) => ({
							id: item.id,
							...(item.data as AuditEntry),
						})),
					};
				},
			},
		},
	});
}

export default createPlugin;

テスト

プラグインを登録した最小限のAstroサイトを作成して、ネイティブプラグインをテストします:

  1. EmDashがインストールされたテストサイトを作成します。
  2. ローカルソースパスから直接インポートして、astro.config.mjs にプラグインを登録します。
  3. 開発サーバーを起動し、コンテンツの作成、更新、削除によってフックをトリガーします。
  4. ctx.log の出力をコンソールで確認し、APIルートを通じてストレージを検証します。

ユニットテストでは、PluginContext インターフェースをモックし、フックハンドラを直接呼び出します。

次のステップ