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

このページ

ネイティブプラグインは、EmDash が Astro サイトと同じプロセスにインポートする npm パッケージです。このチュートリアルでは、コンテンツの保存をログに記録するプラグインを作成し、サイトにインストールして、astro.config.mjs に登録します。

React 管理コンポーネント、Astro レンダリングコンポーネント、信頼できるページフラグメントなど、インプロセスの機能がプラグインに必要な場合は、ネイティブ形式を使用します。フック、ルート、ストレージ、Block Kit で機能をまかなえる場合は、サンドボックス化プラグインから始めてください。プラグイン形式の選択で各形式を比較しています。

前提条件

pnpm を使用し、開発サーバーを実行できる EmDash サイトから始めます。

コマンドでは、サイトのディレクトリを my-emdash-site とし、その隣に plugin-activity を作成します。my-emdash-site は、お使いのサイトのディレクトリ名に置き換えてください。

パッケージを作成して登録する

  1. サイトの隣にネイティブパッケージのディレクトリを作成します。

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. パッケージのメタデータとビルドスクリプトを追加します。

    {
        "name": "@example/plugin-activity",
        "version": "0.1.0",
        "type": "module",
        "main": "./dist/index.mjs",
        "exports": {
            ".": {
                "types": "./dist/index.d.mts",
                "import": "./dist/index.mjs"
            }
        },
        "files": ["dist"],
        "scripts": {
            "build": "tsdown src/index.ts --format esm --dts --clean",
            "dev": "tsdown src/index.ts --format esm --dts --watch",
            "typecheck": "tsc --noEmit"
        },
        "peerDependencies": {
            "emdash": "*"
        },
        "devDependencies": {
            "emdash": "*",
            "tsdown": "^0.20.0",
            "typescript": "^5.9.0"
        }
    }

    npm パッケージ名は @example/plugin-activity です。以下のランタイムコードでは、プラグイン API ルートの URL に収まるよう、スコープなしのプラグイン ID plugin-activity を使用します。

  3. TypeScript の設定を追加します。

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "preserve",
            "moduleResolution": "bundler",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true,
            "outDir": "./dist",
            "rootDir": "./src"
        },
        "include": ["src/**/*"],
        "exclude": ["node_modules", "dist"]
    }
  4. パッケージの依存関係をインストールします。

    pnpm install
  5. コンテンツ保存フックを含む src/index.ts を作成します。

    import { definePlugin } from "emdash";
    import type { PluginDescriptor } from "emdash";
    
    export interface ActivityPluginOptions {
        logUpdates?: boolean;
    }
    
    export function activityPlugin(
        options: ActivityPluginOptions = {},
    ): PluginDescriptor<ActivityPluginOptions> {
        return {
            id: "plugin-activity",
            version: "0.1.0",
            format: "native",
            entrypoint: "@example/plugin-activity",
            options,
        };
    }
    
    export function createPlugin(options: ActivityPluginOptions = {}) {
        return definePlugin({
            id: "plugin-activity",
            version: "0.1.0",
            capabilities: ["content:read"],
            hooks: {
                "content:afterSave": async (event, ctx) => {
                    if (!event.isNew && options.logUpdates === false) return;
    
                    ctx.log.info("Content saved", {
                        collection: event.collection,
                        contentId: event.content.id,
                        isNew: event.isNew,
                    });
                },
            },
        });
    }
    
    export default createPlugin;

    content:afterSave には content:read 機能が必要です。この機能がない場合、EmDash はフックをスキップします。

  6. パッケージをビルドします。

    pnpm build
  7. ローカルパッケージをサイトにインストールします。

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. ディスクリプターファクトリーを EmDash インテグレーションに登録します。

    import { defineConfig } from "astro/config";
    import emdash from "emdash/astro";
    import { activityPlugin } from "@example/plugin-activity";
    
    export default defineConfig({
        integrations: [
            emdash({
                plugins: [activityPlugin({ logUpdates: true })],
            }),
        ],
    });

    ネイティブのディスクリプターは sandboxed ではなく plugins に指定します。sandboxed 配列にネイティブのディスクリプターを指定すると、EmDash は拒否します。

  9. サイトを起動し、管理パネルでエントリを保存します。

    pnpm dev

    サーバーログには、コレクション、コンテンツ ID、エントリが新規作成されたかどうかとともに Content saved が出力されます。

ディスクリプターとランタイムの境界

パッケージのエクスポートには 2 つの役割があります。EmDash は、それぞれを別の段階で使用します。

  • ディスクリプターファクトリーの activityPlugin() は、Astro が設定を評価している間に実行されます。シリアライズ可能なビルド時メタデータ(id、version、format、entrypoint、options)を返します。React と Astro のエントリポイントも、このディスクリプターに含めます。
  • 名前付きエクスポートの createPlugin() は、EmDash の初期化時に実行されます。EmDash は entrypoint からこれをインポートし、シリアライズされた options を渡して、definePlugin() が解決したプラグインを期待します。

名前付きエクスポートの createPlugin は必須です。デフォルトエクスポートはパッケージの利用者には便利かもしれませんが、EmDash のネイティブローダーは createPlugin を名前でインポートします。

id と version は、ディスクリプターと definePlugin() で同一にしてください。プラグイン ID には、plugin-activity のようなスコープなしのケバブケースを使用し、npm スコープはパッケージ名と entrypoint に残します。こうすると、ID を API ルート URL の単一のプラグインセグメントとして使えます。

プラグインの識別とバージョニングに、使用できる ID とバージョンの形式が記載されています。

ランタイムの動作は definePlugin() に記述します。

  • capabilities と allowedHosts
  • storage
  • hooks と routes
  • admin の設定、ページ、ウィジェット、Portable Text の宣言

ディスクリプターには、Astro がビルド時にインポートまたは公開する必要のある静的なエントリを含めます。どの管理フィールドで、ディスクリプターとランタイムの宣言を対応させる必要があるかは、各トピックのガイドを参照してください。

ネイティブルートハンドラー

ネイティブルートハンドラーは、単一の RouteContext を受け取ります。これは、検証済みの入力とリクエストデータを、通常の PluginContext と組み合わせたものです。

routes: {
	status: {
		permission: "plugins:read",
		handler: async (ctx) => ({
			pluginId: ctx.plugin.id,
			callerId: ctx.user?.id ?? null,
		}),
	},
},

同等のサンドボックス化ハンドラーは、(routeCtx, ctx) を 2 つの引数として受け取ります。認証、権限、入力スキーマ、ルート URL については、それ以外は共通の API ルートの規約に従います。

ネイティブルートが request.body を宣言する場合は、definePluginRoute() でラップします。このヘルパーは、 ボディモードから ctx.input を推論します。response: "raw" を持つネイティブルートは pluginResponse() を返します。 どちらのヘルパーも emdash からインポートします。共通の API ルートガイドに、ボディモード、制限、レスポンス ポリシー、互換性のデフォルトが記載されています。

シークレットとバインディングを読み取る

ネイティブプラグインのフックハンドラーとルートハンドラーは、Astro コンテキストではなくプラグインコンテキストを受け取るため、その中では Astro.locals を使えません。シークレットとプラットフォームのバインディングは、ランタイム環境から読み取ってください。

デプロイのシークレットは process.env から読み取ります。Node.js では、ホスティングプラットフォームのシークレットマネージャーが提供します。Cloudflare Workers では、wrangler secret put で設定したシークレットは、Worker のシークレットで説明されているとおり、互換性日付が 2025-04-01 以降の nodejs_compat フラグを通じて process.env に渡されます。

サイト管理者が入力する認証情報には、代わりに生成される設定フォームで secret フィールドを宣言します。EmDash は、保存前に EMDASH_ENCRYPTION_KEY で値を暗号化するため、管理者がフィールドを保存する前にこのキーを設定してください。プラグインは ctx.settings.get() で値を読み取ります。

Cloudflare Workers では、キュー、R2 バケット、send_email などのバインディングは、cloudflare:workers の env 上のオブジェクトです。Astro は Node.js で astro.config.mjs を読み込み、そのファイルはディスクリプターファクトリーのためにプラグインパッケージをインポートします。Node.js は cloudflare:workers を読み込めないため、モジュールの先頭ではなくハンドラー内でインポートしてください。次のフックは、保存された各エントリの ID を、ACTIVITY_QUEUE としてバインドされたキューに送信します。

hooks: {
	"content:afterSave": async (event) => {
		const { env } = await import("cloudflare:workers");
		await env.ACTIVITY_QUEUE.send({ contentId: event.content.id });
	},
},

cloudflare:workers モジュールは Workers ランタイムでのみ解決されるため、これをインポートするプラグインは、Cloudflare アダプターを使用するサイトでのみ動作します。

このインポートの型チェックを行うには、@cloudflare/workers-types をパッケージの devDependencies と、tsconfig.json の compilerOptions.types に追加します。次に、プラグインが読み取る各バインディングを Cloudflare.Env インターフェースで宣言します。次の宣言は、前の例のキューに型を付けます。

declare namespace Cloudflare {
	interface Env {
		ACTIVITY_QUEUE: Queue;
	}
}

ほかのサーフェスを追加する