第 7 章 动手实战(三):配置、组合、HMR 与诊断
本章对应官方教程第 5、6 章,覆盖三件事:配置校验、把 cordis.yml 当作应用来组合与热重载、诊断"永远不加载"的插件。
7.1 配置校验
cordis.yml 中的每个配置项都可携带 config 块;插件声明一个 schema,在运行 apply 之前验证它。配置错误会导致加载失败并给出准确报错——插件绝不会在配置不完整时启动。
创建 config-demo.ts:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
export interface Config {
greeting: string
targets: string[]
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
for (const target of config.targets) {
console.log(`${config.greeting}, ${target}!`)
}
}
关键点:导出的 Config 既是 TypeScript 接口,也是同名的运行时 schema——消费方获得类型,Cordis 获得验证器。dsh 使用 Schemastery;Cordis 本身接受任意 Standard Schema 验证器,因此把普通对象导出为 Config 是不行的。
配置并运行:
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
Hello, alpha!
Hello, beta!
未提供 greeting,schema 默认值补齐——apply 始终收到完整且经过验证的配置。
明确报错
传入无效内容:
- name: './config-demo.ts'
config:
targets: 'not-an-array'
ValidationError: invalid config:
- $.targets expected array but got not-an-array (at targets)
插件的 fiber 进入 FAILED 状态,启动器打印错误后以状态码 1 退出。若配置通过了 schema 但引用的资源/提供方不可用,插件也应在能解析该引用时立即拒绝。
计算得到的配置值:!!js
- name: './config-demo.ts'
config:
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
!!js 仅在 config 与条目 disabled 字段内有效;disabled: !!js ... 在每次挂载决策时基于 loader 上下文求值,可按平台/环境门控一行配置;其余元数据保持静态。
7.2 组合:配置项不只有 name
Cordis 配置项还接受其他元数据:
- id: greeter
name: './greeter.ts'
- id: consumer
name: './consumer.ts'
disabled: true
id 为条目提供稳定标识,让 loader 能区分"修改现有条目"与"先删除再添加"。
disabled: true 卸载插件但不删除条目;改回后插件及所有因依赖其服务而 PENDING 的插件都会重新加载。
组(group)可嵌套一份子列表并作为整体加载/卸载;isolate 为组提供某项服务名的独立实例——两个组可以各自看到配置不同的 shell 提供方,互不影响。
7.3 热模块替换(HMR)
卸载会释放 effect,加载遵循依赖关系,因此 HMR 可以"先卸载、再加载"来替换运行中的插件。@deepseek-ai/cordis-plugin-hmr 监视文件并在保存时执行这一过程。
- id: logger
name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
name: '@deepseek-ai/cordis-plugin-hmr'
config:
root: ['.']
- id: hello
name: './hello.ts'
两个陷阱要注意:
HMR 通过 Cordis logger 服务记录日志——没有 console logger 就看不到它的消息;
HMR 会 inject timer 服务做去抖——没有 timer 插件它就永远停在 PENDING,且不发出任何提示。
在 tsx 下运行,编辑 hello.ts 保存:
hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin
旧实例先卸载(effect 回卷),新代码加载,apply 重新运行。编辑 cordis.yml 本身也会触发更新:loader 按 id 比较条目,只挂载/卸载/重配发生变化的部分。这就是要显式写 id 的原因——不带 id 的条目每次读取都会获得新 id,配置文件任何编辑都会让它被视为"先删除再添加"而重新挂载。
7.4 诊断:永远无法加载的插件
依赖驱动加载的另一面:如果 inject 指定了无人提供的服务,插件会一直等待、不输出任何内容。这不是错误——PENDING 是合法状态,提供方可能稍后才挂载。
创建 diagnose.ts 直接查看插件注册表:
import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
setTimeout(() => {
for (const runtime of ctx.registry.values()) {
for (const fiber of runtime.fibers) {
if (fiber.state === FiberState.PENDING) {
console.log(`${fiber.name} is PENDING — a required service is missing`)
}
}
}
}, 500)
}
再创建一个依赖无法满足的插件:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'needs-timer'
export const inject = ['timer']
export function apply(ctx: Context) {
console.log('needs-timer loaded')
}
- name: './needs-timer.ts'
- name: './diagnose.ts'
运行输出:
needs-timer is PENDING — a required service is missing
修复:向列表添加 - name: '@deepseek-ai/cordis-plugin-timer',插件即加载。
排查口诀:插件既不执行也不报错 → 检查 fiber 状态。不加 PENDING 过滤遍历注册表时,还会看到 Loader、Include 自身处于 ACTIVE——配置文件本身也是通过插件挂载的。
依据官方教程第 5、6 章(05-config、06-composition-and-hmr)整理。