迁移到插件 CLI

本页内容

本指南面向针对先前 definePlugin() 形态编写的 沙箱 插件作者。请按顺序处理破坏性变更。它们都不会改变 hook 或路由在运行时的行为;它们改变的是插件的声明、构建与发布方式。

有关每个包的完整变更列表,请参阅 发布页面 上的对应条目。

破坏性变更

重命名:@emdash-cms/registry-cli 现为 @emdash-cms/plugin-cli

早期版本将 CLI 作为 @emdash-cms/registry-cli 发布,二进制为 emdash-registry。

该包现为 @emdash-cms/plugin-cli,二进制为 emdash-plugin。旧包不再发布。

我该做什么?

替换依赖:

pnpm remove @emdash-cms/registry-cli
pnpm add -D @emdash-cms/plugin-cli

在所有调用处将 emdash-registry 替换为 emdash-plugin。每个子命令保持名称不变(bundle、publish、login、whoami、switch、validate),并新增 init、build 和 dev。参见 The plugin CLI。

重命名:capability 名称使用 resource-first 拼写

早期清单使用诸如 read:content 和 network:fetch 的 capability 名称。创作清单仅接受当前名称,不过运行时在兼容窗口内仍会规范化已发布包中的旧名称。

我该做什么?

在清单中替换每个旧名称:

早期名称当前名称
network:fetchnetwork:request
network:fetch:anynetwork:request:unrestricted
read:contentcontent:read
write:contentcontent:write
read:mediamedia:read
write:mediamedia:write
read:usersusers:read
email:providehooks.email-transport:register
email:intercepthooks.email-events:register
page:injecthooks.page-fragments:register

对 network:request 使用非空的 allowedHosts 列表。仅当操作员在运行时选择目标时,才对 network:request:unrestricted 使用空列表。Capabilities and security 说明了当前权限与网络规则。

变更:沙箱插件使用显式 SandboxedPlugin 注解

早期版本用从 emdash 导入的 definePlugin() 包装插件的 hook 与路由,并由手工注解每个处理程序的参数。

沙箱插件将其定义赋给 SandboxedPlugin 类型的常量,并将该常量作为 default 导出。用 import type 从 emdash/plugin 导入该类型;打包器会擦除该导入。同一子路径还导出轻量运行时辅助函数 pluginRoute() 和 pluginResponse()。TypeScript 从 hook 或路由名称推断每个处理程序的 event 和 ctx,因此处理程序参数无需注解。显式注解还能在隔离的包管理器布局下保持生成声明的可移植性。

我该做什么?

对插件源文件做四处更改。替换导入:

import { definePlugin, type ContentHookEvent, type PluginContext } from "emdash";
import type { SandboxedPlugin } from "emdash/plugin";

将 definePlugin() 包装器替换为显式类型的常量:

export default definePlugin({ /* hooks, routes */ });
const plugin: SandboxedPlugin = { /* hooks, routes */ };

export default plugin;

从每个处理程序中移除参数注解:

handler: async (event: ContentHookEvent, ctx: PluginContext) => {
handler: async (event, ctx) => {

结果是一个 default 导出的对象:

import type { SandboxedPlugin } from "emdash/plugin";

const plugin: SandboxedPlugin = {
	hooks: {
		"content:beforeSave": {
			handler: async (event, ctx) => {
				return event.content;
			},
		},
	},
};

export default plugin;

要在辅助函数中命名事件类型,请从 emdash/plugin 导入:

import type { ContentHookEvent, PluginContext } from "emdash/plugin";

处理程序的 event 始终是该 hook 的规范类型。用更窄的接口注解处理程序将不再通过类型检查。对依赖的字段在运行时用 typeof 检查或守卫进行校验,这是对来自类型系统之外的数据的正确做法。

变更:插件是一个 src/plugin.ts 加上 emdash-plugin.jsonc

早期版本将插件拆成两个文件:src/index.ts 返回 PluginDescriptor(id、version、capabilities、storage、entrypoint),src/sandbox-entry.ts 保存 hook 与路由。

插件现在是一个运行时文件 src/plugin.ts(hook 与路由),以及一个手写清单 emdash-plugin.jsonc(身份与信任契约)。entrypoint 和 format 字段已移除;构建会接线它们。

我该做什么?

按上述形态将 hook 与路由移入 src/plugin.ts。将描述符元数据移入 package.json 旁的 emdash-plugin.jsonc。描述符 id 成为清单 slug;capabilities、allowedHosts 和 storage 保持形态;version 从 package.json 读取,因此省略它。

以下示例显示声明了一个存储集合的描述符的清单等价物:

{
	"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",

	"slug": "plugin-hello",
	"publisher": "did:plc:abc123def456",

	"license": "MIT",
	"author": { "name": "Jane Doe", "url": "https://example.com" },
	"security": { "email": "security@example.com" },

	"capabilities": [],
	"allowedHosts": [],
	"storage": { "events": { "indexes": ["timestamp"] } }
}

每个字段见 The plugin manifest,publisher 字段见 Publisher pinning。

在 package.json 中,将 "./sandbox" 导出口指向构建后的运行时文件:

"./sandbox": "./dist/sandbox-entry.mjs"
"./sandbox": "./dist/plugin.mjs"

将清单加入 files,以便随包一起分发:

"files": ["dist"]
"files": ["dist", "emdash-plugin.jsonc"]

变更:用 emdash-plugin build 构建

早期版本用手工编写的 tsdown 脚本构建两个源文件。

emdash-plugin build 读取 emdash-plugin.jsonc 和 src/plugin.ts 并输出 dist/ 产物。emdash-plugin dev 监视并重建。

我该做什么?

替换构建脚本并添加监视脚本:

"scripts": {
	"build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean"
	"build": "emdash-plugin build",
	"dev": "emdash-plugin dev"
}

然后校验并构建:

emdash-plugin validate
emdash-plugin build

移除:从 emdash 导出的标准格式类型与函数

早期版本从 emdash 导出 StandardPluginDefinition、StandardHookHandler、StandardHookEntry、StandardRouteHandler、StandardRouteEntry 以及函数 isStandardPluginDefinition。

这些已移除。它们是先前 definePlugin 形态的辅助别名。

我该做什么?

对相同目的使用 emdash/plugin 中的 SandboxedPlugin。沙箱插件的导出定义已由其 SandboxedPlugin 注解定型,因此没有 isStandardPluginDefinition 的替代;如需识别插件,请按结构({ hooks?, routes? })判断。

重命名:sandbox-runner 句柄使用 SandboxedPluginInstance

这仅影响自定义 SandboxRunner 的作者,例如 @emdash-cms/cloudflare。大多数插件作者可跳过。

面向作者的 SandboxedPlugin 类型可从 emdash/plugin 创作入口获得。SandboxRunner.load 返回的运行时句柄从 emdash 导出为 SandboxedPluginInstance。

我该做什么?

若你从 emdash 导入 SandboxedPlugin 以类型化 sandbox runner 或持有运行时插件句柄,请将导入改为 SandboxedPluginInstance:

import type { SandboxedPlugin } from "emdash";
import type { SandboxedPluginInstance } from "emdash";

告知用户

安装你插件的站点也需要更改导入。引导他们使用新形态:去掉花括号和 ()。

import { helloPlugin } from "@my-org/plugin-hello";
import hello from "@my-org/plugin-hello";

export default defineConfig({
	integrations: [
		emdash({
			sandboxed: [helloPlugin()],
			sandboxed: [hello],
		}),
	],
});

若插件通过工厂接受配置,请将该配置移到管理设置页面,并从 ctx.settings 读取。沙箱插件描述符是普通对象,无法接收构造函数选项。参见 Settings。

验证已迁移的插件

运行插件测试、校验创作清单,并执行完整的构建与打包检查:

pnpm test
pnpm exec emdash-plugin validate
pnpm exec emdash-plugin build
pnpm exec emdash-plugin bundle --validate-only

然后将本地包安装到开发站点,并演练每个已迁移的 hook 与路由。构建可以确认名称与形态,但无法确认路由返回预期数据,或 hook 正确保留内容。

下一步