关于 setup-ts-deep-modules
为 TypeScript 仓库配置 dependency-cruiser,以强制执行深模块边界。它设置公开根目录入口、私有子目录、循环依赖检查、测试与文档。
主要功能
- 01配置 dependency-cruiser 边界规则
- 02限制外部导入仅使用包入口
- 03允许包内文件相互导入
- 04检测依赖循环
- 05用失败的深层导入测试验证规则
- 06添加包脚手架与边界文档
适用场景
- 01在 TypeScript 仓库强制包封装
- 02阻止深层导入与依赖循环
- 03建立可复用的包布局与检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ leandrocfe/ skills --skill setup-ts-deep-modules安装后在 Claude Code 中输入/setup-ts-deep-modules确认已加载。
附带文件agents/openai.yamldependency-cruiser.config.cjs
详细介绍
设置 TS 深层模块
让此仓库中的每个 package 都成为一个深层模块(deep module):在小巧的接口背后封装大量行为。package 的公共表面是它的 entry points——package 根目录中的文件;子文件夹中的所有内容都隐藏起来。此 skill 会安装 dependency-cruiser 并配置规则,让 entry points 成为唯一的入口,然后验证这些规则确实生效。
关于相关术语(deep module、interface、seam、depth),请运行 skill /codebase-design,并在整个过程中使用其中的语言。
此配置强制采用的格式
src/packages/
<name>/
index.ts ← um entry point (público). Importe este de fora.
client.ts ← outro entry point. Packages podem expor VÁRIOS.
lib/ ← implementação: escondida de fora, livre pra importar entre si.
tests/ ← testes co-localizados + fixtures (uma subpasta, então privada).
公共表面是 package 的根目录文件,而不是指定的 index.ts。按照惯例,实现在 lib/ 中,测试在 tests/ 中,这样每个 package 都有相同的双文件夹格式。不过,规则本身是通用的:任何子文件夹中的任何内容都是私有的,因此你绝不会为了添加文件夹而扩展配置。
四条规则,全部设为 error:
- Entry-point boundary — package 外部的代码(应用代码或其他 package)只能导入该 package 的 entry points(其根目录文件),不能导入其子文件夹中的任何内容。
- Intra-package freedom — package 自身的文件可以自由相互导入。
- Tests through the entry points —
<pkg>/tests/下的文件可以导入任何 package 的 entry points,以及自身tests/中的 fixtures,但绝不能导入任何 package 子文件夹中的内部内容(包括自身的)。跨 package 的集成测试没问题;deep imports 不行。 - No cycles — 不允许依赖循环。
Entry points,而不是 barrel。 因为公共表面是所有根目录文件,package 可以公开多个小型 entry points(index.ts、client.ts、server.ts),而不是将所有内容都汇聚到一个庞大的 index.ts 中。不建议使用重新导出整个子树的 barrel 文件——保持 entry points 精简,并将实现在子文件夹中隐藏起来。
分层(哪些 packages 可以依赖哪些 packages)是另一项关注点,在配置中以注释 stub 的形式保留,供此仓库后续填写。
步骤
1. 检测环境
- Package manager —
pnpm-lock.yaml→ pnpm,yarn.lock→ yarn,bun.lockb→ bun,否则使用 npm。以下所有命令都使用检测到的 package manager(pnpm/yarn/npm run/bunx)。 - Packages 根目录 — 如果存在
src/,则使用src/packages,否则使用packages。如果仓库已有其他明显约定,请与用户确认选择。 - 现有配置 — 检查是否存在
.dependency-cruiser.*文件。如果存在,不要覆盖:合并四条规则和相关选项,并告知用户添加了哪些内容。
完成条件: 已知 package manager、packages 根目录以及现有配置状态。
2. 安装 dependency-cruiser
使用检测到的 package manager,将 dependency-cruiser 安装为 devDependency。
完成条件: 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或添加 path aliases。 - 如果没有统一检查脚本,则添加
lint:boundaries,并告知用户将其加入 CI。
完成条件: lint:boundaries 已存在,且会作为与 typecheck 相同命令的一部分运行。
5. 搭建示例 package 的脚手架
创建一个已提交到仓库的 <packages-root>/example/,作为可复制的模板:
index.ts— 一个入口点。导出一个委托给内部文件的函数(这样 package 就能明显体现出 深模块 的特性,而不是简单转发)。lib/impl.ts— 一个位于子文件夹中的内部文件,由index.ts导入,外部无法访问。tests/example.test.ts— 只导入../index(一个入口点),并对公开函数进行断言。
告诉用户这是一个可复制或删除的初始模板。
完成标准: 示例 package 已存在,通过根入口点公开其行为,并将 impl 隐藏在子文件夹中。
6. 验证规则确实生效
这是整个 skill 的完成标准——无法在违规时失败的配置毫无价值。
- 运行
lint:boundaries。干净的示例应当通过。 - 临时在
tests/example.test.ts中添加一个深层导入(例如:import { thing } from "../lib/impl")。再次运行lint:boundaries——应当因tests-through-entrypoints而失败。 - 撤销深层导入。再次运行——应当通过。
完成标准: 你已观察到先通过,再因深层导入而失败,最后再次通过。如果第 2 步没有失败,说明规则未正确启用——请先修复,再结束。
7. 记录约定
在packages 文件夹中编写一个 README.md(<packages-root>/README.md)——放在它所约束的 packages 旁边,涵盖以下内容:src/packages/<name>/ 布局(根目录下的入口点、用于实现的 lib/、用于测试的 tests/)、“只通过 package 的入口点(其根目录下的文件)导入”,以及如何运行 lint:boundaries。明确不建议使用 barrel 文件——应公开多个小型入口点,而不是通过一个 index 重新导出整个子树。在可复制模板中保留这四条规则,每条写成一个段落。
接着,从仓库的 Agent 指令文件中添加一个上下文指针指向该文件——若存在 CLAUDE.md,则使用它;否则使用 AGENTS.md(如果都不存在,则创建 AGENTS.md)。一行即可,例如:Packages 是深模块——添加或导入 package 前,请参阅 [src/packages/README.md](./src/packages/README.md)。 这样 Agent 就能发现边界规则,而不是意外触犯它。
完成标准: <packages-root>/README.md 已存在且明确不建议使用 barrel;仓库的 CLAUDE.md/AGENTS.md 链接到该文件。
备注
- 配置中的反向引用
$1(dependency-cruiser 的分组匹配)让 package 可以访问自身内部文件,而外部无法访问——不要把它拆成针对每个 package 的独立规则。 - 公开与私有由深度决定:package 根目录下的文件是入口点;子文件夹中的任何内容都是私有的。惯用子文件夹是
lib/(实现)和tests/,但规则不会将其硬编码——任何子文件夹都是私有的,因此新增文件夹永远不需要修改配置。添加入口点只需在根目录中添加文件——无需使用 barrel。 - Packages 采用扁平结构:根目录下只有一层直接子项。package 的内部文件可以按需任意嵌套;package 内不能包含另一个 package。
- 使用
.cjs(而不是.js),这样配置中的module.exports在"type": "module"的仓库中也能正常工作。