mcpField
S
setup-ts-deep-modules

setup-ts-deep-modules

github.com/mattpocock开发305.7k381.0k 次安装1 天前更新
代码仓库

关于 setup-ts-deep-modules

用于在 TypeScript 仓库中配置 dependency-cruiser、落实深模块边界的编码技能。它引导 Agent 设置和验证规则,并记录包入口与私有实现的约定。

主要功能

  1. 01安装并配置 dependency-cruiser
  2. 02强制包入口边界
  3. 03允许包内文件互相导入
  4. 04禁止依赖循环
  5. 05通过故意违规验证规则
  6. 06记录包结构约定

适用场景

  1. 01为 TypeScript 单仓库建立边界
  2. 02防止测试依赖私有实现
  3. 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 级别:

  1. 入口点边界:包外部的代码(应用代码或其他包)只能导入该包的入口点(即根文件),不得导入其子文件夹中的任何内容。
  2. 包内自由:包自身的文件可以相互自由导入。
  3. 测试通过入口点:<pkg>/tests/ 下的文件可以导入任意包的入口点以及其自身的 tests/ 测试夹具,但不得导入任何包的子文件夹内部实现(包括其自身的子文件夹)。跨包集成测试是允许的,深层导入则不允许。
  4. 无循环依赖:不允许存在依赖循环。

入口点,而非 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. 验证规则确实生效

这是整个技能的完成标准:一个对违规行为不报错的配置毫无价值。

  1. 运行 lint:boundaries。在干净的示例上它必须通过。
  2. 临时在 tests/example.test.ts 中添加一个深层导入(例如 import { thing } from "../lib/impl")。再次运行 lint:boundaries,它必须因 tests-through-entrypoints 而失败。
  3. 撤销该深层导入。再运行一次,它必须通过。

完成标准: 你已观察到先通过、再因深层导入而失败、然后再次通过的完整过程。如果第 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 也能正常工作。

评论

支持 Markdown