原生插件是安装在宿主项目中、并在 astro.config.mjs 中注册的 npm 包。该包需要一个已构建的服务端入口,用于提供其描述符和 createPlugin()。如果它还附带 React 或 Astro 组件,请将这些组件作为单独的源码入口导出,以便宿主能够针对正确的环境编译它们。
包布局
下面的布局将服务端运行时与浏览器和 Astro 源码分开:
plugin-activity/
├── src/
│ ├── index.ts
│ ├── admin/
│ │ ├── index.tsx
│ │ └── ActivityPage.tsx
│ └── astro/
│ ├── index.ts
│ └── ActivityBlock.astro
├── dist/
│ ├── index.mjs
│ └── index.d.mts
├── package.json
├── tsconfig.json
└── README.md
dist/ 是生成的目录。请在发布的 tarball 中保留 src/admin/ 和 src/astro/,因为宿主的 Vite 和 Astro 构建必须处理这些入口。
包导出
下面的 package.json 会构建服务端入口,并发布全部三个入口:
{
"name": "@example/plugin-activity",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.mjs",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
},
"./admin": "./src/admin/index.tsx",
"./astro": "./src/astro/index.ts"
},
"files": ["dist", "src/admin", "src/astro"],
"scripts": {
"build": "tsdown src/index.ts --format esm --dts --clean",
"dev": "tsdown src/index.ts --format esm --dts --watch",
"typecheck": "tsc --noEmit",
"prepublishOnly": "pnpm typecheck && pnpm build"
},
"peerDependencies": {
"@cloudflare/kumo": "*",
"@emdash-cms/admin": "*",
"@lingui/core": "*",
"@lingui/react": "*",
"@tanstack/react-query": "*",
"astro": ">=6.0.0-beta.0",
"emdash": "*",
"react": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"tsdown": "^0.20.0",
"typescript": "^5.9.0"
},
"keywords": ["emdash", "emdash-plugin"],
"license": "MIT"
}
如果插件没有受信任的 React UI,请移除 ./admin、src/admin 以及仅用于管理后台的 peer 依赖。如果插件没有 Portable Text 渲染器,请移除 ./astro、src/astro 以及 astro peer。对于导出的源码入口所导入的每一个由宿主持有的库,都要添加对应的 peer 依赖;这样可以防止管理后台包中混入第二份 React、Kumo、Lingui 或 React Query 实例。
这些入口有不同的使用方:
| 导出 | 何时必需 | 使用方 |
|---|---|---|
. | 始终必需 | Astro 配置导入描述符工厂;EmDash 在运行时导入具名的 createPlugin()。 |
./admin | 设置了 adminEntry 时 | 宿主的浏览器构建会导入 React 组件映射。 |
./astro | 设置了 componentsEntry 时 | 宿主的 Astro 构建会导入 blockComponents。 |
描述符和运行时中的模块说明符必须与这些导出一致:
export function activityPlugin(): PluginDescriptor {
return {
id: "plugin-activity",
version: "0.1.0",
format: "native",
entrypoint: "@example/plugin-activity",
adminEntry: "@example/plugin-activity/admin",
componentsEntry: "@example/plugin-activity/astro",
};
}
export function createPlugin() {
return definePlugin({
id: "plugin-activity",
version: "0.1.0",
admin: {
entry: "@example/plugin-activity/admin",
},
});
}
请保持 npm 包版本、描述符版本和 definePlugin() 的版本同步。站点管理员看到的版本来自插件定义,而不会自动取自 package.json。
插件标识与版本
definePlugin() 接受两种 ID:由小写字母、数字和连字符组成的无作用域 ID,或形如 @scope/name 的带作用域 ID。站点插件请使用无作用域的 kebab-case ID,因为该 ID 还会在 /_emdash/api/plugins/<plugin-id>/<route> 中占据一个路径段。
下面的值展示了可接受的形式,以及插件 ID 与 npm 包名之间推荐的区分方式:
id: "plugin-activity"; // Recommended: valid in plugin route URLs
id: "@example/plugin-activity"; // Accepted by definePlugin(), but not one URL segment
entrypoint: "@example/plugin-activity"; // The npm package may stay scoped
版本必须以语义化的 major.minor.patch 序列开头。描述符和运行时都请使用完整的语义化版本:
version: "1.0.0"; // Valid
version: "1.2.3-beta.1"; // Valid prerelease
version: "1.0"; // Invalid: missing patch version
TypeScript 配置
下面的 tsconfig.json 适用于包含 React 和 Astro 源码的原生插件:
{
"compilerOptions": {
"target": "ES2022",
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"types": ["astro/client"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
打包之前,请针对这些源码入口运行 pnpm typecheck。build 脚本只编译 src/index.ts;宿主在使用该包时,才会编译导出的管理后台和 Astro 源码。
检查包
发布之前,请测试 tarball 的确切内容。下面的命令假定有一个名为 my-emdash-site 的一次性站点,与插件目录并列放置。
-
构建并对包进行类型检查。
pnpm typecheck pnpm build -
创建 npm tarball,并查看 npm 打印出的文件列表。
npm pack对于示例包,npm 会创建
example-plugin-activity-0.1.0.tgz。 -
确认输出中包含
dist/index.mjs、dist/index.d.mts,以及可从导出的./admin和./astro模块访问到的每一个源文件。 -
将生成的 tarball 安装到一次性的 EmDash 站点中。
cd ../my-emdash-site pnpm add ../plugin-activity/example-plugin-activity-0.1.0.tgz -
参照创建并注册包,在该一次性站点的
astro.config.mjs中导入并注册描述符工厂。然后构建宿主站点。pnpm build打开每一个插件管理界面,并渲染每一个贡献的 Portable Text 区块。在构建之前先注册,可以让 Astro 解析 tarball 的
./admin和./astro导出;仅测试服务端的包测试无法发现缺失的浏览器源码或.astro源文件。
README 内容
请给运营者提供足够的信息,使其无需阅读源码就能安装和评估该包。内容应包括:
- 一句话描述以及所支持的 EmDash 版本
- 安装命令以及完整的
astro.config.mjs注册方式 - 原生信任边界,以及插件为什么需要原生执行
- 每一项已声明的能力和允许的主机,以及使用它的功能
- 设置及其默认值
- 所需的布局组件,例如用于 body-end 片段的
EmDashBodyEnd - 需要运营者操作的变更所对应的升级步骤
不要把能力声明描述为隔离边界。它们只是限制对 ctx API 的访问,但原生代码仍然可以使用宿主进程可用的导入、环境变量和直接网络调用。
发布到 npm
tarball 测试通过后再发布:
npm publish --access public
带作用域的包首次公开发布时需要 --access public。后续版本请使用语义化版本。对于构造函数选项、已存储数据、所需的宿主变更、包导出或插件信任要求的变化,请将其视为兼容性决策。如果升级需要新的能力或新的允许主机,即使原生安装没有能力授权提示,也请在发布说明中明确指出。
从 npm 安装
运营者在 EmDash 站点中安装已发布的包:
pnpm add @example/plugin-activity
然后按照创建并注册包中所示,在 astro.config.mjs 中导入并注册其描述符工厂。仅安装依赖并不会激活插件;只有修改 Astro 配置并部署站点,安装才算完成。
针对宿主站点开发
以监听模式构建插件:
pnpm dev
在宿主站点中安装本地目录:
pnpm add ../plugin-activity
在 astro.config.mjs 中注册插件的描述符工厂,然后启动宿主的开发服务器。更改描述符元数据或包导出之后,请重启服务器。如果在你的环境中,包管理器的 file 依赖是复制文件而不是链接文件,请在重新构建后重新安装它;而使用工作区依赖或 pnpm link,可以在开发期间保持本地包的连接。
注册表边界
原生包无法发布到 EmDash 注册表。注册表插件使用沙箱包格式、签名发布工作流和安装授权流程。如果插件不再需要 React 管理后台代码、Astro 渲染器、受信任的片段或其他进程内依赖,请在通过注册表发布之前,先将其转换为沙箱格式。