问题修复
在 TypeScript…
当 TypeScript 仓库按包组织、希望包的公共表面仅为根入口文件且隐藏子文件夹实现时,安装并配置 dependency-cruiser 验证边界。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 TypeScript 多包仓库中强制通用
解决思路
- 开始时调用 Skill tool,参数为 "codebase-design",并沿用其中的 deep module、interface、seam、depth 等术语。
- 采用目标形态:src/packages/<name>/ 下,index.ts、client.ts 等根文件为入口点;lib/ 存实现,tests/ 存测试;子文件夹对外私有。
- 将包的公共表面定义为该包的所有根文件,而不是单个指定的 index.ts;包可暴露多个小入口点。
- 按“入口点边界”配置规则:包外代码(应用代码或其他包)只能导入该包的入口点(根文件),不能导入其子文件夹中的任何内容。
- 按“包内自由”配置规则:包自己的文件之间可以自由互相导入。
- 按“测试通过入口点”配置规则:<pkg>/tests/ 下的文件可以导入任何包的入口点以及自己的 tests/ fixtures,但不能导入任何包的子文件夹内部(包括自己的)。
- 按“无循环依赖”配置规则:禁止依赖循环。
- 将上述四条规则都设为 error 级别。
- 不要用 barrel 汇总整个子树;保持入口点小而分散;若需要暴露多个能力,新增根文件作为入口点,而不是做成一个巨大的 index.ts。
- 将分层(哪些包可依赖哪些包)视为另一个问题,仅在配置中留注释 stub,交由仓库后续填写。
- 检测包管理器:pnpm-lock.yaml → pnpm,yarn.lock → yarn,bun.lockb → bun,否则 npm;之后所有命令使用对应命令。
- 检测 packages 根目录:若存在 src/ 则用 src/packages,否则用 packages;若仓库已有明显不同约定,先与用户确认。
- 检查是否存在 .dependency-cruiser.* 配置;若存在,不要覆盖,把四条规则和 options 合并进去,并告诉用户新增了什么。
- 确认已完成检测:包管理器、packages 根目录、现有配置状态均已知。
- 使用检测到的包管理器将 dependency-cruiser 安装为 devDependency。
- 确认 dependency-cruiser 已出现在 devDependencies。
- 将 dependency-cruiser.config.cjs 复制到仓库根目录为 .dependency-cruiser.cjs。
- 在 .dependency-cruiser.cjs 中将 PACKAGES_ROOT 设置为步骤 1 检测到的根目录。
- 规则基于路径深度且与扩展名无关,因此除 PACKAGES_ROOT 外无需其他适配。
- 确认 .dependency-cruiser.cjs 存在且 PACKAGES_ROOT 正确,并包含四条 forbidden rules。
- 添加 lint:boundaries 脚本:depcruise <packages-root>,或 depcruise src。
- 将 lint:boundaries 并入仓库已有的总检查命令(例如已运行 typecheck 的 check / ci / validate 脚本)。
- 不要修改 tsconfig,也不要添加 path aliases。
- 如果没有总检查脚本,则添加 lint:boundaries,并告诉用户将其纳入 CI。
- 确认 lint:boundaries 存在,并与 typecheck 在同一个命令中运行。
- 创建已提交的 <packages-root>/example/ 作为可复制模板。
- 在 example 中创建 index.ts 作为入口点,导出一个函数并委托给内部文件,使包明显是 deep 模块而不是透传。
- 创建 lib/impl.ts 作为子文件夹中的内部文件,由 index.ts 导入,且外部不可达。
- 创建 tests/example.test.ts,只导入 ../index(入口点),并针对公共函数做断言。
- 告知用户 example 是起始模板,可复制或删除。
- 确认示例包存在、通过根入口点暴露行为,并将 impl 隐藏在子文件夹中。
- 运行 lint:boundaries,必须先在干净的示例上 pass。
- 临时在 tests/example.test.ts 中加入深层导入,例如 import { thing } from "../lib/impl";再次运行 lint:boundaries,必须以 tests-through-entrypoints 失败。
- 撤销该深层导入;再次运行,必须再次 pass。
- 确认已观察到 pass → 因深层导入 fail → 再次 pass;若第 2 步不失败,说明规则未正确接入,必须在完成前修复。
- 在 packages 文件夹中写入 <packages-root>/README.md,说明 src/packages/<name>/ 布局:入口点在根目录,lib/ 为实现,tests/ 为测试;只通过包的入口点(根文件)导入;以及如何运行 lint:boundaries。
- 在 README 中明确劝阻 barrel files:暴露多个小入口点,而不是通过一个 index 重新导出整个子树;保持内容为可复制片段加四条规则各一段。
- 从仓库的 agent-instructions 文件添加上下文指针:若存在 CLAUDE.md 则用它,否则用 AGENTS.md;若两者都不存在则创建 AGENTS.md。一行即可,例如 Packages are deep modules: see [src/packages/README.md](./src/packages/README.md) before adding or importing one.
- 确认 <packages-root>/README.md 存在且劝阻 barrels,并且仓库的 CLAUDE.md/AGENTS.md 链接到它。
- 不要将配置中的 $1 反向引用(dependency-cruiser 的 group matching)拆成单独的按包规则;它们让包能访问自身内部而外部不能。
- 按深度区分 public/private:包的根文件是入口点;任何子文件夹内容都是私有。约定子文件夹为 lib/ 和 tests/,但规则不硬编码它们;新增文件夹永远不需要改配置。
- 新增入口点就是新增根文件,无需 barrel。
- 包是扁平的:packages 根目录下只有一层直接子项;包内部可以任意嵌套;包不能包含另一个包。
- 使用 .cjs(不是 .js)编写配置,以便在 type 为 module 的仓库中 module.exports 仍可用。
适用边界 · 注意事项
- 仓库不是 TypeScript,或不按包/模块组织且不打算建立 packages 结构时,不要使用这套做法。
- 如果无法新增 devDependency 或不准备在本地/CI 中运行边界检查,则不要使用,因为该技能的价值依赖 lint:boundaries 能失败。
- 如果已有 .dependency-cruiser.* 配置且不允许合并/修改,不要覆盖;先与用户或团队确认。