| name | migrate-zhin-plugin-runtime |
| description | Migrate legacy Zhin.js plugins and projects to the convention-based Plugin Runtime — from usePlugin/getPlugin/addCommand/addMiddleware/addComponent/addTool/addCron/declareConfig/useContext and mutable registries to definePlugin + capability directories + Scope/Token resources. Use this whenever a Zhin plugin fails to load under `zhin runtime start`, errors with "does not default-export a Plugin definition", still imports zhin.js/@zhin.js/core/@zhin.js/kernel, or when asked to upgrade, port, modernize, or migrate a Zhin plugin, command, middleware, component, tool, cron job, config schema, or package manifest — even if the user does not say the word "migrate". 适用于旧 Zhin 插件向约定式目录与快照运行时的破坏性迁移。 |
迁移 Zhin Plugin Runtime
目标是产出纯新架构代码:plugin.ts 只做装配,能力按目录发现,共享状态走 Resource/Token。
不保留 compat runtime,不双写。
先建立事实,再动手
迁移最容易翻车的地方是凭印象改代码。zhin runtime migrate status 会静态分析整个项目并
返回一个状态机,它比任何猜测都准 —— 每一步都以它的输出为准:
zhin runtime migrate status
| state | 含义 | 下一步 |
|---|
blocked | 有 error 或 manual 诊断,自动迁移无法证明语义等价 | 人工清掉诊断,见 人工诊断处理 |
extraction-required | 还有能自动搬运的注册(automatic > 0) | zhin runtime migrate extract --write |
cutover-required | 能力已就位,但 package.json#zhin / plugin.ts 还没生成 | zhin runtime migrate cutover --write |
dual-run | 仍在从 zhin.js 导入经典 API(usePlugin / MessageCommand 等),或直接 import @zhin.js/core / @zhin.js/kernel | 改用门面约定 API(definePlugin / defineCommand 等),删掉旧入口 |
compat | 仍在 import @zhin.js/next-compat | 移除 compat 依赖 |
ready | 完成 | 跑构建与测试 |
状态是从上往下判定的:只要还有 manual/error 诊断就一直是 blocked,先清诊断再谈其它。
工作流
- 盘点:读目标包的 README、最近的测试、旧入口,弄清用户可见行为(命令、消息、定时、
持久化)。迁移的验收标准是行为不变,不是编译通过。
- 看计划:
zhin runtime migrate extract --check,逐条读 changes 与 diagnostics。
--check 与 --write 必须二选一,同时给或都不给会直接报错。
- 搬能力:
zhin runtime migrate extract --write。它只搬模块顶层、且闭包干净的注册,
已存在的目标文件不会被覆盖。
- 清诊断:每条
manual 都要人工处理,见 人工诊断处理。
最常见的是 action 捕获了模块级变量 —— 把它提升为 owner Resource,能力文件再从执行上下文读。
- 装配:
zhin runtime migrate cutover --write 生成 package.json#zhin 与 plugin.ts,
并补齐 zhin.js、@zhin.js/runtime;Stable Features 可由 Root 继承,不必再装 @zhin.js/command|middleware|component(cutover 仍可能按约定目录写入 features 挂载)。启动脚本统一是
zhin runtime start,不要再写失效的 zhin dev / zhin start / zhin build。
package.json#private: true 的本地 TS root 使用 entry: "./plugin.ts",直接执行
pnpm dev 或 zhin runtime start。
- 非 private 的发布包使用
entry: "./plugin.js";cutover 生成独立的
tsconfig.zhin.json、zhin:build 与 prepack / prepublishOnly,以便 pnpm pack
和 npm publish 前把 plugin.ts、约定目录和 src/ 编译成可发布 JS。不要把
plugin.ts 作为发布 manifest 的入口。
已有合法 manifest 会被补齐到相应模式;其它 zhin 字段形态仍需人工处理。
- 迁移剩余配置:
schema.json(只声明本包字段)、Feature mounts、child plugin mounts。
- 删旧:删掉旧注册代码、旧入口、compat 依赖。
- 验证:构建 + 测试 + 行为验证(命令路由、消息发送、配置默认值、热更新)。
第 2–5 步之间反复跑 status 是最省事的做法 —— 它会告诉你还差什么。
目标写法
plugin.ts 只装配;能力一个文件一个,default export。完整对照见
迁移映射。
import { createToken, definePlugin, databaseHostToken } from 'zhin.js';
export const storeToken = createToken<Store>('my-plugin.store');
export default definePlugin({
name: 'my-plugin',
setup(context) {
if (!context.resources.has(databaseHostToken)) return;
const db = context.resources.use(databaseHostToken);
context.resources.provide(storeToken, createStore(db));
return () => { };
},
});
import { defineCommand } from 'zhin.js/command';
import { storeToken } from '../plugin.js';
export default defineCommand({
description: 'Show current user profile',
async execute(context) {
const store = context.use(storeToken);
return store.describe(context.input.sender.id);
},
});
两个 context 不是同一个东西,别混用:
setup(context) 里 context.resources 是 Scope —— provide / use / has 都挂在它上面。
能力的 execute(context) 拿到的是 CapabilityContext —— 直接 context.use(token) /
context.config,没有 context.resources,写成 context.resources.use(...) 会在运行期
报 Cannot read properties of undefined。
硬性规则
这些不是风格偏好,违反会让迁移在运行期而不是编译期爆炸:
usePlugin() / getPlugin() 不得出现在能力执行路径。 新运行时不建立
AsyncLocalStorage 上下文,调用它只会拿到一个挂空的孤儿 Plugin,注册的东西永远不生效。
- 不双写。 同一能力不要既留旧 registry 又建新目录,
status 会一直停在 dual-run。
- 不引入
@zhin.js/next-* 或 legacy callback adapter。
- Command 参数只由文件名表达,不要在 metadata 里维护第二套路由。
- 消息发送必须走统一 render/send 链路,不要在能力里直连 endpoint。
- 自动迁移无法证明语义等价时,保留 diagnostic 人工改写,不要做猜测性替换。
- 每个
register / 订阅都要有 disposer 交给 context.lifecycle,否则热更新会重复注册。
完成标准
zhin runtime migrate status
rg -n "usePlugin\(|getPlugin\(|add(Command|Middleware|Component|Tool|Cron)\(|@zhin.js/next-" .
pnpm --filter <plugin-package> build
pnpm --filter <plugin-package> test
pnpm check:plugin-runtime-migration-readiness
pnpm check:plugin-runtime-migration-verify
公开插件还必须验证 tarball,而不是只验证源码类型检查:
pnpm --filter <plugin-package> run build
pnpm --filter <plugin-package> pack
rg 只允许命中文档与迁移测试。ready 意味着静态检查通过,不等于行为等价 —— 平台相关
行为(真实适配器收发、定时触发)要么实测,要么在交付说明里写清未验证项,不要用"编译通过"
替代运行时验证。
仓库门禁
仓库内声明 zhin.type: "plugin" 的包会由 check:plugin-runtime-migration-readiness 做确定性检查。
它只扫描当前 checkout,不读取 cwd 之外的用户项目;tests/、test/、fixtures/、__fixtures__/
和带 zhin-migration-gate: legacy-fixture 标记的源码会被排除。因此迁移示例可以保留旧 API,
但 native Plugin Runtime 的生产源码不能在函数体内调用 usePlugin() 或 getPlugin()。
离线 Verify
pnpm check:plugin-runtime-migration-verify 是 migration 的离线 E2E verify:先确认 cutover
已经无变更,再用不触发 install 的 pnpm run build 验证构建。私有 development root 额外要求
scripts.dev 与 scripts.start 都是 zhin runtime start;公开 publish package 会执行
pnpm pack,解读 tarball 后确认 package.json#zhin.entry、
plugin.js、plugin.d.ts 和每个已发现能力目录的 JS 产物一致。它不执行 install,也不会访问网络。