关于 setup-ts-deep-modules
用于在 TypeScript 仓库中配置 dependency-cruiser、落实深模块边界的编码技能。它引导 Agent 设置和验证规则,并记录包入口与私有实现的约定。
主要功能
- 01安装并配置 dependency-cruiser
- 02强制包入口边界
- 03允许包内文件互相导入
- 04禁止依赖循环
- 05通过故意违规验证规则
- 06记录包结构约定
适用场景
- 01为 TypeScript 单仓库建立边界
- 02防止测试依赖私有实现
- 03验证导入限制能够拦截违规
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mattpocock/ skills --skill setup-ts-deep-modules安装后在 Claude Code 中输入/setup-ts-deep-modules确认已加载。
附带文件agents/openai.yamldependency-cruiser.config.cjs
详细介绍
设置 TS 深模块
让本仓库中的每个包都成为深模块:在小而简洁的接口背后承载大量行为。包的公共接口是其入口点(包根目录下的文件),其子文件夹中的所有内容都是隐藏的。本 skill 会安装 dependency-cruiser 以及使入口点成为唯一访问途径的规则,然后验证这些规则确实生效。
有关术语(深模块、接口、接缝、深度),请调用 Skill 工具并传入 "codebase-design",并在整个过程中使用其中的用语。
此规则强制的结构
src/packages/
<name>/
index.ts ← an entry point (public). Import this from outside.
client.ts ← another entry point. Packages may expose SEVERAL.
lib/ ← implementation: hidden from outside, free to import each other.
tests/ ← co-located tests + fixtures (a subfolder, so private).
公共接口是包的根文件,而不是某个指定的 index.ts。按照惯例,实现代码位于 lib/ 中,测试位于 tests/ 中,使每个包都具有相同的两层文件夹结构。不过规则本身是通用的:任何子文件夹中的任何内容都是私有的,因此你无需为了新增文件夹而修改配置。
四条规则,均为 error 级别:
- 入口点边界:包外部的代码(应用代码或其他包)只能导入该包的入口点(即根文件),不得导入其子文件夹中的任何内容。
- 包内自由:包自身的文件可以相互自由导入。
- 测试通过入口点:
<pkg>/tests/下的文件可以导入任意包的入口点以及其自身的tests/测试夹具,但不得导入任何包的子文件夹内部实现(包括其自身的子文件夹)。跨包集成测试是允许的,深层导入则不允许。 - 无循环依赖:不允许存在依赖循环。
入口点,而非 barrel 文件。 由于公共接口是所有根文件,包可以暴露多个小型入口点(例如 index.ts、client.ts、server.ts),而无需将所有内容汇总到一个巨大的 index.ts 中。不建议使用重新导出整棵子树的 barrel 文件;应保持入口点精简,并将实现隐藏在子文件夹中。
分层(哪些包可以依赖哪些包)是另一个问题,在配置中以注释桩的形式保留,供本仓库自行补充。
步骤
1. 检测环境
- 包管理器:存在
pnpm-lock.yaml则使用 pnpm,存在yarn.lock则使用 yarn,存在bun.lockb则使用 bun,否则使用 npm。以下所有命令都使用它(pnpm/yarn/npm run/bunx)。 - 包根目录:若存在
src/,则使用src/packages,否则使用packages。若仓库已有其他明显的约定,请与用户确认选择。 - 现有配置:检查是否存在
.dependency-cruiser.*文件。若存在,不要覆盖它:将四条规则和相关选项合并进去,并告知用户你添加了哪些内容。
完成标准: 已确定包管理器、包根目录以及现有配置的情况。
2. 安装 dependency-cruiser
使用检测到的包管理器将 dependency-cruiser 安装为开发依赖。
完成标准: dependency-cruiser 已位于 devDependencies 中。
3. 编写配置
将 dependency-cruiser.config.cjs 复制到仓库根目录,并命名为 .dependency-cruiser.cjs。将 PACKAGES_ROOT 设置为第 1 步中检测到的根目录。这些规则基于路径深度且与文件扩展名无关,因此无需其他适配。
完成标准: .dependency-cruiser.cjs 已存在,PACKAGES_ROOT 正确,且四条禁止(forbidden)规则均已存在。
4. 接入检查流程
- 添加
lint:boundaries脚本:depcruise <packages-root>(或depcruise src)。 - 将其合并到仓库现有的综合检查命令中,即运行类型检查(typecheck)的那个命令(例如
check/ci/validate脚本)。不要修改tsconfig,也不要添加路径别名。 - 若不存在综合检查脚本,则添加
lint:boundaries,并告知用户将其纳入 CI。
完成标准: lint:boundaries 已存在,并且与类型检查在同一条命令中运行。
5. 搭建示例包的脚手架
在版本库中创建一个已提交的 <packages-root>/example/,作为可复制的模板:
index.ts是入口文件。导出一个函数,并将其委托给内部文件实现(这样包明显是深模块,而不是简单转发)。lib/impl.ts:位于子文件夹中的内部文件,由index.ts导入,外部无法直接访问。tests/example.test.ts仅导入../index(入口文件),并针对公开函数进行断言。
告知用户这是一个入门模板,可以复制或删除。
完成标准: 示例包已存在,通过根入口文件暴露其行为,并将 impl 隐藏在子文件夹中。
6. 验证规则确实生效
这是整个技能的完成标准:一个对违规行为不报错的配置毫无价值。
- 运行
lint:boundaries。在干净的示例上它必须通过。 - 临时在
tests/example.test.ts中添加一个深层导入(例如import { thing } from "../lib/impl")。再次运行lint:boundaries,它必须因tests-through-entrypoints而失败。 - 撤销该深层导入。再运行一次,它必须通过。
完成标准: 你已观察到先通过、再因深层导入而失败、然后再次通过的完整过程。如果第 2 步没有失败,说明规则未正确配置,需先修复再结束。
7. 记录约定
在包文件夹中编写 README.md(<packages-root>/README.md,与其管理的包并列放置),内容包括:src/packages/<name>/ 的结构(入口文件位于根目录,lib/ 存放实现,tests/ 存放测试)、"只能通过包的入口文件(其根目录文件)导入",以及如何运行 lint:boundaries。明确反对使用桶文件:应暴露多个小型入口文件,而不是通过一个 index 重新导出整个子树。内容保持精简,即一段可复制的代码片段加上四条规则,每条规则各占一段。
然后在仓库的 Agent 指令文件(若存在 CLAUDE.md 则使用它,否则使用 AGENTS.md;两者都不存在时创建 AGENTS.md)中添加一个上下文指引,指向该 README。一行即可,例如:Packages are deep modules: see [src/packages/README.md](./src/packages/README.md) before adding or importing one. 这样 Agent 才能发现边界规则,而不是在违反时才发现。
完成标准: <packages-root>/README.md 已存在且明确反对桶文件,并且仓库中的 CLAUDE.md/AGENTS.md 链接到了它。
注意事项
- 配置中的
$1反向引用(dependency-cruiser 的分组匹配)使包能够访问自身内部,而外部无法访问。不要将其拆分为每个包单独的规则。 - 公开还是私有由深度决定:包的根目录文件是入口文件,子文件夹中的任何内容都是私有的。常见的子文件夹是
lib/(实现)和tests/(测试),但规则并未硬编码它们:任何子文件夹都是私有的,因此新增文件夹无需修改配置。新增入口只需在根目录添加文件(无需桶文件)。 - 包是扁平的:根目录下只有一层直接子项。包的内部结构可以任意嵌套,但包不得包含其他包。
- 使用
.cjs(而非.js),这样即使在"type": "module"的仓库中,配置中的module.exports也能正常工作。