setup-ts-deep-modules
将 dependency-cruiser 接入 TypeScript 仓库,使每个包都成为深层模块:将实现隐藏在子文件夹中,并且只能通过入口文件访问。由用户调用。
Setup TS Deep Modules - TypeScript 深模块边界配置
技能概述
Setup TS Deep Modules 为 TypeScript 仓库接入 dependency-cruiser,把每个包变成「深模块」——大量行为藏在子文件夹里,对外只暴露包根目录那几个入口点文件,并确保越界导入会直接让检查失败。
适用场景
- TypeScript monorepo 的包边界治理:包与包之间、应用代码与包之间互相深层导入,改一个内部文件就得全局搜索调用点。这个技能把「只能从入口点进」变成机器可执行的规则,而不是靠口头约定和代码评审。
- 团队里有人在用 AI 编码代理写代码:代理倾向于直接 import 到最具体的实现文件,因为那样「看起来最直接」。技能给出的四条 error 级规则会在 lint 阶段拦住这类深层导入,等于给代理装了一道可自动发现的红线。
- 想让每个包的设计质量可衡量:深模块的标准是「接口小、行为多」。入口点就是接口,
lib/就是实现。包一旦按这个形状组织,重构内部实现不再需要动外部调用方,包也更容易被单独测试和替换。
核心功能
-
环境检测与依赖安装:先判断仓库用的是 pnpm、yarn、bun 还是 npm(看 lockfile),再确定包根目录(有
src/就是src/packages,否则packages),然后用对应的包管理器把 dependency-cruiser 装成 devDependency。如果仓库已经存在.dependency-cruiser.*配置文件,技能不会覆盖它,而是把四条规则和选项合并进去,并告诉你加了什么。 -
写入四条边界规则并接入检查命令:生成的
.dependency-cruiser.cjs里四条规则全部是 error 级别——- 入口点边界:包外的代码(应用代码或其他包)只能导入该包的入口点(它的根文件),不能碰它子文件夹里的任何东西。
- 包内自由:包自己的文件之间随便互相导入,不受限制。
- 测试走入口点:
tests/下的文件可以导入任意包的入口点、以及自己tests/里的 fixture,但不能导入任何包的子文件夹内部实现,连自己包的也不行。跨包写集成测试没问题,深层导入不行。 - 禁止循环:不允许出现依赖环。
同时新增
lint:boundaries脚本并把它并进仓库已有的 typecheck 总命令(check/ci/validate之类),不动 tsconfig、不加路径别名。 -
脚手架示例包并证明规则真的生效:技能会创建一个可复制可删除的
example包作为模板——根目录index.ts是入口点并委托给lib/impl.ts(所以它是真的「深」,不是一层透传),tests/example.test.ts只导入../index。随后它会跑三遍验证:干净状态必须通过,临时加一行import { thing } from "../lib/impl"必须失败并报出tests-through-entrypoints,撤销后必须再次通过。只有亲眼看到「通过 → 失败 → 通过」,这个技能才算完成。最后它会在包目录下写一份README.md说明约定(并明确劝退 barrel 文件),再从仓库的CLAUDE.md/AGENTS.md加一行指向它的链接。
常见问题
什么是深模块?和普通的包拆分有什么区别?
深模块指的是「大量行为藏在一个很小的接口后面」。普通的包拆分常常只解决了文件放在哪,包的公共接口仍然是散开的——外部代码可以 import 到包内任何一个文件,于是包的内部结构事实上变成了公共 API,想重构都动不了。深模块把这条线划死:包的根文件是入口点,也就是公共接口;任何子文件夹里的东西都是实现,外部一律进不来。结果是接口小而稳定,实现可以自由重写。
为什么公共接口是「包根目录的所有文件」,而不是一个 index.ts?
因为单一 index.ts 容易退化成 barrel 文件——把所有子模块重新导出一遍。这样一来入口点名义上只有一个,实际公共接口却和整个子树一样大,深模块的好处就没了。这个技能把公共面定义为包根目录下的每一个文件,所以一个包可以暴露好几个小入口点(index.ts、client.ts、server.ts),每个都小而聚焦。反过来,barrel 式的整树再导出是被明确劝退的。另外这也意味着:新增一个入口点就是新增一个根文件,不需要动配置。
新增一个实现子文件夹需要改配置吗?
不需要。判断公共还是私有靠的是路径深度:包的根文件是入口点,任何子文件夹里的东西都是私有的。配置里不硬编码 lib/ 和 tests/,它们只是约定俗成的两个文件夹(实现放 lib/,测试放 tests/),你新加一个 internal/ 或 adapters/ 同样自动私有。配置能同时做到「包内自由、包外受限」,靠的是 dependency-cruiser 的 $1 分组反向引用,所以也不要把它拆成每个包一条规则。
使用限制
- 需要你主动调用:这个技能是 user-invoked 的,不会自己触发。
- 面向已有的包结构:它假定仓库已经(或即将)按
<packages-root>/<name>/的扁平结构组织——包根下只有一层直接子目录,包里面不能再套包。如果仓库已有明显不同的约定,技能会先和你确认包根目录该放哪。 - 不负责分层:哪个包可以依赖哪个包是另一件事,配置里只留了一段注释桩给仓库自己填。
- 用 pnpm / yarn / bun 时命令会跟着变:技能会自己检测,但你得先有 lockfile 它才认得出来。