分发原生插件

本页内容

原生插件是安装在宿主项目中、并在 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 的一次性站点,与插件目录并列放置。

  1. 构建并对包进行类型检查。

    pnpm typecheck
    pnpm build
  2. 创建 npm tarball,并查看 npm 打印出的文件列表。

    npm pack

    对于示例包,npm 会创建 example-plugin-activity-0.1.0.tgz。

  3. 确认输出中包含 dist/index.mjs、dist/index.d.mts,以及可从导出的 ./admin 和 ./astro 模块访问到的每一个源文件。

  4. 将生成的 tarball 安装到一次性的 EmDash 站点中。

    cd ../my-emdash-site
    pnpm add ../plugin-activity/example-plugin-activity-0.1.0.tgz
  5. 参照创建并注册包,在该一次性站点的 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 渲染器、受信任的片段或其他进程内依赖,请在通过注册表发布之前,先将其转换为沙箱格式。