技能setup-ts-deep-modules
S

setup-ts-deep-modules

将 dependency-cruiser 接入 TypeScript 仓库,使每个包都成为深层模块:将实现隐藏在子文件夹中,并且只能通过入口文件访问。由用户调用。

Setup TS Deep Modules - TypeScript 深模块边界配置

技能概述

Setup TS Deep Modules 为 TypeScript 仓库接入 dependency-cruiser,把每个包变成「深模块」——大量行为藏在子文件夹里,对外只暴露包根目录那几个入口点文件,并确保越界导入会直接让检查失败。

适用场景

  1. TypeScript monorepo 的包边界治理:包与包之间、应用代码与包之间互相深层导入,改一个内部文件就得全局搜索调用点。这个技能把「只能从入口点进」变成机器可执行的规则,而不是靠口头约定和代码评审。
  2. 团队里有人在用 AI 编码代理写代码:代理倾向于直接 import 到最具体的实现文件,因为那样「看起来最直接」。技能给出的四条 error 级规则会在 lint 阶段拦住这类深层导入,等于给代理装了一道可自动发现的红线。
  3. 想让每个包的设计质量可衡量:深模块的标准是「接口小、行为多」。入口点就是接口,lib/ 就是实现。包一旦按这个形状组织,重构内部实现不再需要动外部调用方,包也更容易被单独测试和替换。

核心功能

  1. 环境检测与依赖安装:先判断仓库用的是 pnpm、yarn、bun 还是 npm(看 lockfile),再确定包根目录(有 src/ 就是 src/packages,否则 packages),然后用对应的包管理器把 dependency-cruiser 装成 devDependency。如果仓库已经存在 .dependency-cruiser.* 配置文件,技能不会覆盖它,而是把四条规则和选项合并进去,并告诉你加了什么。

  2. 写入四条边界规则并接入检查命令:生成的 .dependency-cruiser.cjs 里四条规则全部是 error 级别——

    • 入口点边界:包外的代码(应用代码或其他包)只能导入该包的入口点(它的根文件),不能碰它子文件夹里的任何东西。
    • 包内自由:包自己的文件之间随便互相导入,不受限制。
    • 测试走入口点tests/ 下的文件可以导入任意包的入口点、以及自己 tests/ 里的 fixture,但不能导入任何包的子文件夹内部实现,连自己包的也不行。跨包写集成测试没问题,深层导入不行。
    • 禁止循环:不允许出现依赖环。

    同时新增 lint:boundaries 脚本并把它并进仓库已有的 typecheck 总命令(check/ci/validate 之类),不动 tsconfig、不加路径别名。

  3. 脚手架示例包并证明规则真的生效:技能会创建一个可复制可删除的 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.tsclient.tsserver.ts),每个都小而聚焦。反过来,barrel 式的整树再导出是被明确劝退的。另外这也意味着:新增一个入口点就是新增一个根文件,不需要动配置。

新增一个实现子文件夹需要改配置吗?

不需要。判断公共还是私有靠的是路径深度:包的根文件是入口点,任何子文件夹里的东西都是私有的。配置里不硬编码 lib/tests/,它们只是约定俗成的两个文件夹(实现放 lib/,测试放 tests/),你新加一个 internal/adapters/ 同样自动私有。配置能同时做到「包内自由、包外受限」,靠的是 dependency-cruiser 的 $1 分组反向引用,所以也不要把它拆成每个包一条规则。

使用限制

  • 需要你主动调用:这个技能是 user-invoked 的,不会自己触发。
  • 面向已有的包结构:它假定仓库已经(或即将)按 <packages-root>/<name>/ 的扁平结构组织——包根下只有一层直接子目录,包里面不能再套包。如果仓库已有明显不同的约定,技能会先和你确认包根目录该放哪。
  • 不负责分层:哪个包可以依赖哪个包是另一件事,配置里只留了一段注释桩给仓库自己填。
  • 用 pnpm / yarn / bun 时命令会跟着变:技能会自己检测,但你得先有 lockfile 它才认得出来。