你的第一个沙箱插件

本页内容

本指南从零开始构建一个最小的沙箱插件。该插件记录每次内容保存并公开一个 API 路由。它在由配置的沙箱运行器提供的隔离运行时中执行。当站点操作员将其从 sandboxed: [] 移动到 plugins: [] 时(例如在没有沙箱运行器的平台上),相同的代码也会在进程内运行。

如果你还没有在沙箱和原生之间做出决定,请先阅读选择插件格式

两个部分

一个沙箱插件包含:

  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 读取它,因此有一个唯一的真实来源。参见清单参考
  • 信任契约是同意。 稍后更改 capabilitiesallowedHostsstorage 需要版本升级 — 已安装的站点已同意旧契约。

编写运行时

src/plugin.ts 默认导出一个用 satisfies SandboxedPlugin 注释的简单对象。emdash/plugin 仅提供类型,因此沙箱插件对 emdash 没有运行时依赖。

以下示例将每次内容保存记录到插件存储中,并公开一个返回最后十次保存的 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(具有完整的规范事件类型)和 ctx 作为 PluginContext,因此处理程序不需要参数注释。像 "content:afterSav" 这样的拼写错误的钩子键是编译错误。
  • 钩子处理程序接受 (event, ctx) 事件形状取决于钩子名称;请参阅钩子指南
  • 路由处理程序接受 (routeCtx, ctx) — 两个参数。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 返回最后十个保存事件。

接下来阅读什么