配置文件
graft start 读取 YAML 文件,默认是
graft.yml。文件内容是一个配置项列表,每项加载一个插件:
- id: database name: "@acme/graft-plugin-postgres" config: url: !env DATABASE_URL
- id: api name: ./plugins/api.mjs config: port: 8080
- id: tenants name: tenants group: true isolate: app/Database: true config: - id: tenant-db name: "@acme/graft-plugin-postgres" config: url: !env TENANT_DATABASE_URL| 本页内容 | 阅读 |
|---|---|
| 配置项的格式 | 配置项字段 |
name 如何找到模块 |
插件名称 |
| 来自环境的密钥和设置 | 环境变量 |
| 编辑器补全和检查 | 编辑器支持 |
| 保存文件时会发生什么 | 在运行期间修改配置 |
| 从插件或代码中编辑配置项 | Loader API |
条目也可以让它的插件在子进程、worker 或另一台机器上运行:写上
name: remote,再把插件的配置包一层,见
从 YAML 加载。它仍然是普通条目,同样有 id、disabled、inject、isolate,也能在运行期间编辑。保存的条目 id 同时也是 runner 上的实例名。
从条目加载的插件可从 @graftland/loader 导入 Entry,读取
yield* Entry.CurrentId。它是
Context.Reference<Option<string>>,值为当前条目的 id,嵌套 group 中也一样;不经过条目加载时默认为
None。插件内部 fork 的子插件会继承它,不会为每个 fork 生成新 id。Include 会把生成的 id 保存到 YAML,包括插件的条目列表,供宿主重启后继续使用。不使用文件时,缺失的 id 由父项 id、插件名称及其在未指定 id 的同名条目中的次序决定。
在顶层声明命令插件条目:
- name: remote/cli cli: true这些条目配置 CLI 命令,graft start 不会把它们加载到应用树中。 config
传给命令插件,disabled: true 禁用条目。修改后重新运行命令即可生效。命令条目的
!env 读取进程环境;start --env-file 和 --env-dir 只作用于应用条目。
| 来源 | 规则 |
|---|---|
| 项目 | 当前目录的 graft.yml,不向父目录查找。 |
| 用户 | ~/.graft/graft.yml,包安装在 ~/.graft/node_modules。 |
| 覆盖目录 | GRAFT_HOME 替换 ~/.graft,同时改变配置和包的目录。 |
| 合并 | name 完全相同时以项目条目为准,包括 disabled;其他条目合并。命令名冲突时报错。 |
| 项目之外 | 用户级命令仍可使用;缺失的配置文件视为空列表。 |
| 路径 | 模块以声明所在的配置文件目录为准;插件可通过 ConfigDirectory 解析配置里的路径。 |
只有显式声明的模块会运行,不扫描已安装依赖。可用
graft add --plugin 一次完成安装和启用。
| 字段 | 作用 |
|---|---|
id |
标识配置项。如果未指定,Graft 会生成一个并写回文件,插件内的条目列表也一样。 |
name |
插件模块,可以是相对路径、包名或短名称。分组会忽略此字段。 |
config |
插件的输入;对于分组,则是其配置项列表。 |
disabled |
设为 true 时卸载该项,但保留在文件中。 |
group |
设为 true 时,该项成为包含其他配置项的分组。 |
inject |
加载插件前需要等待的服务键,作为插件自身 inject 声明的补充。 |
isolate |
需要隔离的键。true 表示私有领域(realm),标签表示共享领域。 |
intercept |
经过 Schema 校验的已登记 Reference 配置补丁,对该项及其后代生效。 |
| 规则 | 说明 |
|---|---|
| ID 唯一 | Loader/Include 要求整棵条目树的 ID 唯一,包括禁用项。插件的条目列表和直接嵌入的 Group.Group 则只要求同层兄弟项 ID 唯一。 |
| 各层级相同 | 所有嵌套层级的配置项字段都遵循相同的解码规则。 |
| 额外字段 | note 等额外字段作为元数据保留,API 编辑时也是如此。它们不会绕过对已声明字段的校验。 |
| 空分组 | 分组省略 config 时,按空分组(config: [])处理;分组的 config 不能显式设为 null。 |
| 键 | 服务键和 Reference 键是创建它们时使用的字符串。例如,Context.Service<...>()("app/Database") 对应的服务键是 app/Database。 |
跨编辑保持身份
Section titled “跨编辑保持身份”插件的条目列表在插入或重排条目时保留已写入的 id。不使用文件时,插入或移动同名条目前请显式指定 id。
重新写入未变化的匿名配置树时,已分配的 ID 和运行中的插件实例保持不变。
- 加载器先恢复子项 ID。
- 再匹配匿名父分组。
- 最后将 ID 写回 YAML。
匿名配置项先按名称和保守的配置指纹筛选,再精确比较;重复匹配项保持原顺序。
配置文件中的 intercept
Section titled “配置文件中的 intercept”配置文件中的 intercept 只接受显式登记的 Reference。graft start
默认登记了两项:
| Reference 键 | 配置值 | 行为 |
|---|---|---|
effect/References/CurrentLogAnnotations |
对象 | 与外层日志注解浅合并,同名键以配置值为准。 |
effect/References/MinimumLogLevel |
All、Fatal、Error、Warn、Info、Debug、Trace 或 None |
替换外层最低日志级别,区分大小写。 |
- id: api name: ./plugins/api.mjs intercept: effect/References/CurrentLogAnnotations: component: api effect/References/MinimumLogLevel: Warn该项及其子插件继承日志注解和级别,兄弟配置项保持各自的设置。此例会过滤
Info、Debug 和 Trace 日志;All 允许所有级别,None 关闭所有日志。
| 问题 | 错误 |
|---|---|
| 键未登记 | UnregisteredReference |
| 值无效 | InvalidIntercept |
嵌入 loader 时,登记表默认为空,需要宿主应用在启动时提供。
name 的解析方式:
name 的形式 |
含义 |
|---|---|
以 . 开头 |
相对于配置文件的路径。 |
以 / 开头 |
绝对路径。 |
以 @ 开头,或包含 : |
直接使用。 |
其他(例如 review) |
先尝试原名称,再尝试 @graftland/plugin-<name> 和 graft-plugin-<name>;原名称对应的模块若没有导出插件(例如 Node 的 http),会被跳过。两种命名约定地位相同:两者都已安装时,该条目以 AmbiguousPluginName 失败。判断时两者都会被导入,因此第二个导入失败时,该条目以该错误失败。 |
包从应用目录解析。例如,name: remote 会找到
@graftland/plugin-remote,name: remote/runner 会找到它的 runner 导出。
模块无法加载时
Section titled “模块无法加载时”如果找不到插件或模块导入失败,该配置项仍会出现在 fork 树中,显示为带错误的失败 fork。当配置项变化,或热重载检测到新代码时,会再次尝试加载。
配置项的生命周期
Section titled “配置项的生命周期”每个启用的配置项都会先创建生命周期 fork,再开始模块解析。
| 情形 | 行为 |
|---|---|
| 导入尚未完成 | 计为 Loading。ready 会继续等待,但健康兄弟项和后续编辑仍能推进。 |
| 配置项被删除、禁用或替换 | 取消其生命周期并释放解析器资源。 |
| 解析期间修改配置 | 启动新一轮解析;未变化的配置项保留当前解析过程。 |
| 已取消导入的迟到结果 | 不会重新激活旧条目。原生 import Promise 本身仍可能继续执行模块代码。 |
| 解析成功 | 生命周期将实际插件作为子 fork 创建。ready 检查这整棵子树。 |
等待服务注入的 Pending fork 和 Failed fork 仍视为已稳定。
Effect importer 的每个模块候选都有独立的子 scope 来取得资源:
- 候选失败后,先释放其资源,再尝试下一个名称。
- 清理失败会阻止回退,并与导入失败的 cause 一并保留。
- 成功候选的资源由配置项的生命周期持续持有。
!env NAME 在加载配置项时读取环境变量。
- 文件中保留原始标签,因此密钥不会被写入文件。
- 如果变量未设置,该项会失败,直到问题修复。
graft start --env-file .env还会从.env文件读取变量;进程环境变量优先。--env-dir以同样的方式读取挂载的密钥文件。详见命令行。
!env
得到的是字符串,最终变成什么由插件的配置 Schema 决定。读取哪个变量由编写配置文件的人决定,插件只描述自己需要的值。
| Schema | 结果 |
|---|---|
Schema.NumberFromString |
数字。 |
Schema.RedactedFromValue(Schema.String) |
日志中会隐藏的密钥。 |
插件如果想用 Effect Config 的组合子(例如默认值和备选来源),可以在 apply
里用它们读取自己的输入:
import { Config, ConfigProvider, Effect, Schema } from "effect"
import { Plugin } from "@graftland/graft"
const Settings = Config.all({ port: Config.Port("port").pipe(Config.withDefault(8080)),})
const readSettings = (input: unknown) => Settings.parse(ConfigProvider.fromUnknown(input))
export default Plugin.make({ name: "app/server", config: Schema.Record(Schema.String, Schema.Unknown), apply: (input) => Effect.gen(function* () { const settings = yield* readSettings(input) yield* Effect.log(`Listening on port ${settings.port}`) }),})
Effect.runSync(readSettings({ port: "3000" })) // => { port: 3000 }Effect.runSync(readSettings({})) // => { port: 8080 }插件的 Schema 仍然描述配置项接受什么。插件自行从环境中的 provider 读取的
Config 不是 !env 标签,retry 不知道它们,也不会重新加载。
标签在哪里有效
Section titled “标签在哪里有效”- 普通插件的
config和intercept值均支持该标签,包括嵌套数组和对象。 - 配置项先解析自身的 intercept,再由已登记的 Schema 解码并合并;解析后的值仍须通过该 Schema 校验。
- 分组只解析自身的 intercept,不解析后代的描述,也不提前解析子项的变量。
- 禁用项不解析变量。
- 启用的子项缺少变量时,只让该子项失败,不影响其分组和兄弟项。
- 已持久化目标和文件均保留原始标签。
何时重新读取
Section titled “何时重新读取”| 触发条件 | 重新读取变量的配置项 |
|---|---|
| 普通 API 编辑或 YAML 变化 | 只有变化的配置项。未变的健康兄弟项和子树保留现有值。 |
loader.retry 或 loader.root.update(entries) |
整棵树,包括嵌套分组和注入门控下的子项。 |
显式刷新之后:
- 运行中配置项的解析结果变了,会通过
update接收新配置。 - 解析后的 intercept 变了,则重新加载该项。
- 值未变的配置项继续运行。
- 仍在导入模块的项会在导入完成后读取环境变量;刷新请求到达时若仍在加载,会在激活后再读一次。
ready也会等待排队中和执行中的刷新,因此显式刷新后再等待ready,所有活跃项都已使用当前值。
数组和普通对象可以共享配置值,但不能成环。
| 环出现的位置 | 结果 |
|---|---|
| config 或 intercept 中 | 该项以 CyclicConfig 失败,兄弟项和后续编辑仍可继续。 |
| 条目/分组结构中 | 在分配 ID 和加载前以 InvalidOperation 拒绝。 |
环境变量解析将其他类实例视为不透明值。
graft schema 写入
graft.schema.json,即针对项目所安装插件的这个文件的 JSON
Schema。在文件第一行用注释把 YAML language server 指向它,VS
Code(装有 YAML 扩展)等编辑器就能补全和检查配置项:
# yaml-language-server: $schema=./graft.schema.json- id: api name: ./plugins/api.mjs| Schema | 检查内容 | 保持更新的方式 |
|---|---|---|
graft.schema.json |
配置项字段、已登记的 intercept,以及每个导出登记项的已安装插件的 config,group 内也一样。 |
graft create;文件存在时还有 graft add 和 graft remove。 |
@graftland/cli/schema.json,随每个版本发布 |
配置项字段和已登记的 intercept。所有插件的 config 都保持开放,包括 Graft 自己的插件。 |
按版本固定,如 https://cdn.jsdelivr.net/npm/@graftland/cli@1.2.3/schema.json。 |
graft create让graft.yml指向./graft.schema.json,并在依赖安装完成后写入该文件。- 把
graft.schema.json提交到仓库,克隆后编辑器即可检查配置项。 - 直接用包管理器安装或升级包之后,自己运行
graft schema。 - 没有登记项的配置保持开放,在启动时由插件自己的 schema 检查。
language server 可能会把 !env 标为错误。需要在编辑器设置中声明这个标签,以 VS
Code 为例:
{ "yaml.customTags": ["!env scalar"] }在运行期间修改配置
Section titled “在运行期间修改配置”使用 graft start --watch
时,Graft 会监听配置文件。Include 合并配置入口及物理目标监听器的通知,连续 100 毫秒没有新通知后才刷新。监听器安装完成后仍会请求补读一次,符号链接切换目标后也一样。
随后 Graft 会比较新的配置项与当前正在运行的配置项:
| 变化 | 结果 |
|---|---|
config 变化 |
通过 update 接收新输入,插件会原地更新或重启。 |
name、group、inject、isolate 或 intercept 变化 |
卸载后重新加载。 |
| 新增配置项 | 加载。 |
| 移除或禁用配置项 | 卸载。 |
| 没有变化的健康配置项 | 继续运行,保留现有环境值。 |
| 参与调和的分组中的失败项 | 重试。尚未完成且未变的解析保留原有激活。 |
- 独立兄弟项的更新与移除并发执行。
- 每个配置项自身的操作仍按顺序执行,替换必须等待该项旧实例清理完毕。
- 分组会等待所有必要操作完成后才接受目标。
- 延迟的环境刷新和普通编辑使用分组的同一个串行协调执行器,不会为了应用更新的目标而中断 update 回调。
- 已移除或过时代次的配置项的请求会被丢弃。
删除、禁用或替换配置项时若清理失败,Graft 会记录完整 cause 及配置项、fork 标识,并继续协调。
- 被移除的 fork 已处置,替换项拥有独立的生命周期。
- 仍保留的 fork 重启失败时,失败会以
Failed状态保持可见,可以重试。 - resolver 观察资源与生命周期清理的失败会一并记录,不重启健康兄弟项。
ready将失败 fork 视为已稳定,不保证清理成功。
| 情形 | 行为 |
|---|---|
| 运行期间文件无效 | Graft 记录错误,并继续运行上一次有效的配置。 |
初始文件无效,使用 --watch |
Graft 记录 cause,以空条目启动并继续监听;修正文件后自动加载。 |
初始文件无效,未使用 --watch |
启动失败。 |
| 文件无效时通过 API 编辑 | 不会将无效文件覆盖为空配置。 |
嵌入 Include 并提供 initial
时,只有 YAML 准备、ID 分配、全树校验及序列化验证全部成功后,才会创建缺失的文件,并一次提交完整文档。无效的初始条目不会留下文件;已有文件优先于
initial。
Include 会保留符号链接。每次磁盘操作先解析配置路径的真实目标,并将该目标与内容快照一起保存。读取、冲突检查、自动补 ID 和 API 编辑都使用同一目标,临时文件也建在目标目录。
| 情形 | 行为 |
|---|---|
| rename 前链接改变 | 丢弃临时文件,沿用有界重试与同字段冲突规则,在新目标上重新应用编辑。 |
| watch 模式 | Include 同时监听入口目录和当前目标目录。 |
| 入口链接改变 | 重新读取,并切换目标监听:关闭旧监听,新监听就绪后再请求一次读取。 |
| 新目标 YAML 无效 | 保留上一次有效的运行树目标;修正该文件后可恢复。 |
| 未开启 watch | 下次 API 编辑会读取新目标。 |
| 位于监听目录之外的中间链接 | 下次读取时生效,但自身不会触发通知。 |
| 普通文件不存在 | 可以初始化。 |
| 断链、循环链接或不可访问的链接 | 返回类型化文件系统错误,不会被替换成普通文件。请先修复目标;API 编辑或入口变化通知都会重新尝试。 |
托管 Remote 条目使用 artifact 和 target,代替 implementation 和
connect:
- id: forecasts name: "@graftland/plugin-remote" config: contract: "@example/forecast-contract" artifact: directory: ./artifacts/sha256-v1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa identity: version: 1 algorithm: sha256 digest: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa entry: dist/index.js target: _tag: Worker probes: [health] maxInFlight: 16 config: {}将示例中的制品替换为 Artifact.capture
返回的目录和标识。制品目录和相对契约路径以配置文件所在目录为基准。子进程使用
target: { _tag: Local }。
条目 id 就是托管实例名称:通过
Graft.transition({ target: "forecasts", candidate })
发起升级。实例名称在整个运行时中必须唯一,包括嵌套 Loader。子注册继承所属条目的标识。
Loader 在写入文件或更改已接受目标前,以 ManagedFork
拒绝托管树的编辑。无法阻止外部手动改写文件;监听刷新会拒绝这些修改并保留原目标。transition 只改变正在运行的代,不会重写
graft.yml。持久化部署意图与升级结果 分开记录。