用途 该技能为 TypeScript 仓库引入依赖边界约束,把每个包变成“深模块”:大量行为藏在小的接口之后。包的公开表面是其入口点(包根目录下的文件),子文件夹中的全部内容视为私有。技能会安装 dependency-cruiser 并写入相应规则,使入口点成为唯一进入方式,并验证规则确实生效。涉及“深模块”“接口”“接缝”“深度”等词汇时,使用 codebase-design 技能的术语体系。
强制的结构 src/packages/ <name>/ index.ts 入口点(公开),供外部导入 client.ts 另一个入口点,包可以暴露多个入口 lib/ 实现,对外隐藏,内部可互相导入 tests/ 就近测试与夹具(子文件夹,因此私有) 公开表面是包的根文件,而非某个指定的 index.ts。按约定实现放在 lib/,测试放在 tests/,让每个包拥有相同的两文件夹形态。但规则本身是通用的:任何子文件夹中的内容都是私有的,因此新增文件夹无需改动配置。
四条规则,全部为 error 级别
- 入口边界:包之外的代码(应用代码或其他包)只能导入该包的入口点(其根文件),不能导入其子文件夹中的任何内容。
- 包内自由:包自身的文件之间可以自由互相导入。
- 测试通过入口:<pkg>/tests/ 下的文件可以导入任意包的入口点以及自己的 tests/ 夹具,但不能导入任何包的子文件夹内部(包括自己的)。跨包的集成测试可以,深层导入不行。
- 禁止循环:不允许依赖循环。
入口点而非 barrel:由于公开表面是每个根文件,一个包可以暴露多个小入口点(index.ts、client.ts、server.ts),而不必把一切都塞进一个庞大的 index.ts。不鼓励通过 barrel 文件重新导出整棵子树;保持入口点小,把实现藏在子文件夹中。
分层(哪些包可以依赖哪些包)是另一个关注点,在配置中以注释桩的形式留给本仓库填写。
步骤
- 探测环境:包管理器依据 pnpm-lock.yaml → pnpm、yarn.lock → yarn、bun.lockb → bun,否则 npm,后续命令统一使用它;包根目录,若存在 src/ 则用 src/packages,否则用 packages,若仓库已有明显不同的既有约定需与用户确认;既有配置,检查是否存在 .dependency-cruiser.* 文件,若存在不要覆盖,而是把四条规则与选项合并进去,并告知用户新增了什么。完成标准:包管理器、包根目录、既有配置状态全部可知。
- 安装 dependency-cruiser:用探测到的包管理器将其装为 devDependency。完成标准:dependency-cruiser 出现在 devDependencies 中。
- 写配置:把 dependency-cruiser.config.cjs 复制到仓库根目录为 .dependency-cruiser.cjs,将 PACKAGES_ROOT 设为第一步探测到的根。规则基于路径深度且与扩展名无关,无需其他适配。完成标准:.dependency-cruiser.cjs 存在且 PACKAGES_ROOT 正确,四条禁止规则齐全。
- 接入检查:添加 lint:boundaries 脚本(depcruise <packages-root> 或 depcruise src);把它并入仓库已有的总检查命令(即已运行 typecheck 的 check / ci / validate 脚本),不要改动 tsconfig 或添加路径别名。若没有总检查脚本,则添加 lint:boundaries 并提示用户纳入 CI。完成标准:lint:boundaries 存在并与 typecheck 在同一命令中运行。
- 生成示例包:创建被提交的 <packages-root>/example/ 作为可复制模板——index.ts 是入口点,导出一个委托给内部文件的函数(使包明显是“深”的,而非透传);lib/impl.ts 是子文件夹中的内部文件,由 index.ts 导入,外部无法访问;tests/example.test.ts 只导入 ../index(入口点)并对公开函数做断言。告知用户这是可复制或删除的起始模板。完成标准:示例包存在,通过根入口点暴露行为,并把实现藏在子文件夹中。
- 证明规则生效(整个技能的完成判据,不会因违规而失败的配置毫无价值):运行 lint:boundaries,干净的示例必须通过;临时在 tests/example.test.ts 中添加深层导入(如 import { thing } from "../lib/impl"),再次运行必须因 tests-through-entrypoints 失败;撤销深层导入,再运行必须通过。完成标准:观察到通过、因深层导入失败、再次通过。若第二步没有失败,说明规则未正确接线,须修复后再结束。
- 记录约定:在包所在文件夹写 README.md(<packages-root>/README.md,与被约束的包相邻),内容涵盖 src/packages/<name>/ 布局(入口点在根、lib/ 放实现、tests/ 放测试)、“只通过包的入口点(其根文件)导入”、以及如何运行 lint:boundaries;明确不鼓励 barrel 文件,主张暴露多个小入口点而非通过一个 index 重新导出整棵子树。篇幅控制在可复制片段加四条规则各一段。然后在仓库的智能体指令文件(若存在 CLAUDE.md,否则 AGENTS.md,两者都不存在则创建 AGENTS.md)中添加一行指向它的上下文指针,例如:Packages are deep modules: see src/packages/README.md before adding or importing one。这样智能体才能发现边界规则而不是踩到它。完成标准:<packages-root>/README.md 存在且不鼓励 barrel,仓库的 CLAUDE.md/AGENTS.md 链接到它。
备注 配置中的 $1 反向引用(dependency-cruiser 的分组匹配)让包能访问自己的内部而外部不能,不要把它们拆成按包分开的规则。公开与私有由深度决定:包的根文件是入口点,子文件夹中的任何内容都是私有;约定子文件夹是 lib/(实现)和 tests/,但规则并不硬编码它们,任何子文件夹都是私有,因此新文件夹永远不需要改配置。添加入口点只是添加一个根文件(不用 barrel)。包是扁平的:根下只有一层直接子项;包的内部可以任意深度嵌套;但一个包不能包含另一个包。使用 .cjs(而非 .js)以便配置中的 module.exports 在 "type": "module" 的仓库中也能工作。