
用 dependency-cruiser 为 TypeScript 仓库强制实施 Deep Modules入口文件边界的完整落地指南【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills本指南基于skills/in-progress/setup-ts-deep-modules/SKILL.md及其配套的 dependency-cruiser.config.cjs讲述如何把 TypeScript 仓库中的每个包改造成“深模块”deep module把大量行为隐藏在少量入口文件之后让包的根目录文件成为唯一对外通道。读完本文你将掌握完整的接线流程、四条边界规则的底层正则原理、如何用一次“故意破坏”验证规则真正生效以及如何把这一约定固化到 Agent 的工作流中。什么是 Deep Module小接口背后的大行为先建立共享词汇。本仓库的 codebase-design 技能为“深模块”提供了精确的术语体系setup-ts-deep-modules 全程使用这套语言模块Module任何同时拥有接口与实现的东西刻意与规模无关可以是一个函数、一个类、一个包也可以是一个跨层切片接口Interface调用方正确使用模块所需知道的一切不只是类型签名还包括不变量、顺序约束、错误模式、所需配置与性能特征深度Depth接口处的杠杆率即调用方或测试每学习一份接口所能调用的行为量。深模块 小接口 大量实现浅模块 大接口 几乎空的实现要避免缝Seam接口所坐落的位置可以在不改动原处的情况下改变行为杠杆Leverage调用方从深度中获得的好处一份实现回馈给 N 个调用点和 M 个测试局部性Locality维护者从深度中获得的好处变更、缺陷、知识与验证集中在一处而不是扩散到所有调用方。正如 codebase-design 指出的深度是接口的属性而非实现的属性一个深模块内部可以继续由许多小而可替换的部件组成它们只是不属于接口而已。setup-ts-deep-modules 所做的正是把“每个包都是深模块”这一设计目标变成可被 CI 强制执行的工程约束。本技能强制塑造的目录形态技能要求仓库形成如下的统一形态src/packages/ name/ index.ts ← 入口文件公开。外部只能从这里 import。 client.ts ← 另一个入口文件。一个包可以暴露多个入口。 lib/ ← 实现对外隐藏内部文件之间可自由互相 import。 tests/ ← 与代码同目录的测试与 fixtures子目录属于私有。核心判断标准有三条公开面 包的根目录文件而不是某个指定的index.ts按惯例实现放在lib/测试放在tests/让每个包都有相同的两文件夹形态规则本身是通用的任何子文件夹里的任何东西都是私有的因此将来新增文件夹时永远不需要改配置。注意这里明确区分了“入口文件”与“桶文件barrel”入口文件而不是桶文件。因为公开面是每一个根目录文件一个包可以暴露多个小而精的入口index.ts、client.ts、server.ts而不是把一切塞进一个巨型index.ts。鼓励保持入口文件小而隐藏实现明确不鼓励那些把整棵子树重新导出的 barrel 文件。分层哪些包可以依赖哪些包是一个独立的关注点配置文件里已为它留好了注释形式的占位见后文。四条边界规则全部 error 级别规则一共四条加上一条禁环全部以error级别生效意味着任何违反都会让lint:boundaries失败入口边界Entry-point boundary包外代码应用代码或另一个包只允许导入该包的入口文件根目录文件绝不能触碰其子文件夹里的任何东西。包内自由Intra-package freedom一个包自己的文件之间可以自由互相导入。测试走入口Tests through the entry pointspkg/tests/下的文件可以导入任意包的入口文件以及自己的tests/fixtures但绝不能导入任何包的子文件夹内部实现包括自己包的。跨包的集成测试没问题深导入不行。禁环No cycles不允许出现依赖环。第四条规则在配置中以no-circular呈现其余三条分别对应配置中的entrypoint-boundary-from-app、entrypoint-boundary-across-packages、tests-through-entrypoints与tests-folder-is-private。注意实际上配置里是5 条 forbidden 规则除了技能正文列出的四条还多出一条tests-folder-is-private一个包的tests/目录只允许测试自身访问防止其他代码误导入测试 fixtures。这四条加一条共同构成了完整的边界体系。七步接线流程技能把整个落地过程拆成七个可验证的步骤每步都带明确的 “Done when” 验收条件。第 1 步探测环境包管理器pnpm-lock.yaml→ pnpmyarn.lock→ yarnbun.lockb→ bun否则 npm。后续所有命令都要用探测到的那个管理器pnpm/yarn/npm run/bunx。包根目录如果存在src/就用src/packages否则用packages。若仓库已有明显不同的惯例应与用户确认。已有配置检查是否存在.dependency-cruiser.*文件。若已存在不要覆盖把四条规则与 options 合并进去并明确告诉用户你添加了什么。验收包管理器、包根目录、已有配置状态三者都已确定。第 2 步安装 dependency-cruiser用探测到的包管理器把dependency-cruiser安装为 devDependency。验收dependency-cruiser出现在devDependencies中。第 3 步编写配置把仓库自带的 dependency-cruiser.config.cjs 复制到仓库根目录并命名为.dependency-cruiser.cjs然后把PACKAGES_ROOT设为第 1 步探测到的根目录。规则基于路径深度且与扩展名无关因此除此之外无需任何适配。验收.dependency-cruiser.cjs存在、PACKAGES_ROOT正确、四条禁止规则齐全。第 4 步接入检查命令新增lint:boundaries脚本depcruise packages-root或depcruise src。把它并入仓库已有的总检查命令那个已经跑 typecheck 的check/ci/validate脚本。不要改动 tsconfig也不要添加路径别名。如果没有总检查脚本就只加lint:boundaries并告知用户应把它纳入 CI。验收lint:boundaries存在并与 typecheck 在同一条命令中执行。第 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 步记录约定并让 Agent 能发现它在packages 文件夹内packages-root/README.md放在它所管辖的包旁边写一个README.md覆盖src/packages/name/的布局根目录是入口、lib/是实现、tests/是测试、只通过包的入口文件根目录文件导入、以及如何运行lint:boundaries。明确反对 barrel 文件宁可暴露多个小入口也不要通过一个 index 重新导出整棵子树。内容保持在复制即用代码片段 四条规则各一段的篇幅。然后从仓库的 Agent 指令文件存在CLAUDE.md就用它否则用AGENTS.md两者都没有就新建AGENTS.md中加一个上下文指针。一行就够例如Packages are deep modules: see [src/packages/README.md](https://link.gitcode.com/i/13e81aed4c1ce9bd479dca9b07ef04fb) before adding or importing one.这就是让 Agent 主动发现边界规则、而不是撞上它才后悔的关键一步。验收packages-root/README.md存在且反对 barrel仓库的CLAUDE.md/AGENTS.md链接到了它。配置文件逐行拆解正则如何区分内外仓库自带的 dependency-cruiser.config.cjs 是整个方案的引擎值得逐段理解。PACKAGES_ROOT 与派生正则/** Where packages live. One immediate child dir per package (flat, no nesting). */ const PACKAGES_ROOT src/packages; // --- derived patterns (no need to edit) ------------------------------------- const R PACKAGES_ROOT; /** * A packages private internals: anything nested inside a package subfolder. * The packages root files are its entry points and are NOT matched here: * they stay importable from outside. */ const PACKAGE_INTERNALS ^${R}/[^/]/[^/]/;唯一的编辑点是PACKAGES_ROOT。PACKAGE_INTERNALS这个正则表达的就是深度决定公私的核心哲学^src/packages/锚定包根目录[^/]匹配第一个目录层级即包名第二个[^/]匹配包内的第一层子文件夹如lib、tests结尾的/匹配子文件夹下的内容。因此凡是匹配PACKAGE_INTERNALS的就是私有内部实现而包根目录文件如index.ts由于后面没有第二层目录不匹配该模式从而保持对外可导入。五条 forbidden 规则逐一解读entrypoint-boundary-from-app应用代码只能走入口from: { pathNot: ^${R}/ }, // 导入方不在任何包内 to: { path: PACKAGE_INTERNALS },任何位于包树之外的文件不得导入任何包内部。entrypoint-boundary-across-packages跨包只能走入口包内自由from: { path: ^${R}/([^/])/, pathNot: ^${R}/[^/]/tests/ }, // 导入方在包 $1 内且非测试 to: { path: PACKAGE_INTERNALS, pathNot: ^${R}/$1/, // 同一包 → 包内自由 },这里的关键是 dependency-cruiser 的组匹配反向引用$1from的捕获组捕获了导入方所属的包名to.pathNot用它放行导入自己包内部的情况。正如技能 Notes 所强调的这个$1反向引用正是自己人进得去、外人进不来的机制所在不要把它拆散成逐包的手写规则。tests-through-entrypoints测试同样走入口from: { path: ^${R}/([^/])/tests/ }, // 测试文件属于包 $1 to: { path: PACKAGE_INTERNALS, pathNot: ^${R}/$1/tests/, // 自己的 tests/ fixtures → 允许 },测试可以导入任意包的入口文件、以及自己tests/目录下的 fixtures但连自己包的lib/都不许深导入——这与 codebase-design 的接口即测试面the interface is the test surface原则一脉相承调用方和测试跨越同一条缝想测试接口背后的东西说明模块的形状可能错了。tests-folder-is-privatetests 文件夹只对测试开放from: { pathNot: ^${R}/[^/]/tests/ }, // 导入方不是测试 to: { path: ^${R}/[^/]/tests/ },防止业务代码顺手 import 测试 fixtures堵住测试代码泄漏进生产路径的口子。no-circular禁环from: {}, to: { circular: true },若只想限制包内出现环可在注释提示下把作用域收窄到^${R}/。options 与分层占位options: { doNotFollow: { path: node_modules }, tsConfig: { fileName: tsconfig.json }, enhancedResolveOptions: { extensions: [.ts, .tsx, .js, .jsx, .json], }, },doNotFollow跳过node_modules避免噪音与误报tsConfig让 dependency-cruiser 使用tsconfig.json做模块解析enhancedResolveOptions.extensions声明参与解析的扩展名集合。配置文件末尾还预留了**分层layering**的注释占位。技能明确区分两个正交的关注点**接口隐藏interface-hiding**控制怎么导入必须走入口分层控制哪个包可以依赖哪个。仓库当前把分层留作注释模板例如// { // name: ui-may-not-depend-on-billing, // severity: error, // from: { path: ^${R}/ui/ }, // to: { path: ^${R}/billing/ }, // },需要时可自行取消注释并填入真实的包名。三条重要设计约束技能 Notes 部分点明了三个容易忽略的设计决策公开 vs 私有由深度决定而非枚举包根目录文件是入口任何子文件夹内容都是私有的。惯用的子文件夹是lib/实现与tests/但规则并不硬编码它们任何子文件夹都是私有的所以新增文件夹永远不需要改配置新增入口也只需添加一个根目录文件无需 barrel。包是扁平flat的根目录下只有一层直接子目录即一个包。包的内部可以任意嵌套多深但一个包内部不能再包含另一个包。用.cjs而非.js这样即使仓库是type: module配置里的module.exports也能正常工作。同理不要使用路径别名去绕过边界——第 4 步明确要求不要改动 tsconfig也不要添加 path aliases否则边界的意义会被别名击穿。与深模块设计体系的衔接setup-ts-deep-modules 是本仓库深模块体系中的落地工具与设计侧的能力形成闭环词汇与判断标准来自 codebase-design它回答了什么样的模块算深、缝该放在哪里深化方法论在 DEEPENING.md按依赖类别进程内、本地可替换、远程但自有的 Ports Adapters、真正的外部 Mock决定如何跨缝测试并强调测试应跨越接口断言可观察结果而不是内部状态——这正是 setup-ts-deep-modules 让测试只能走入口的深层动机接口的多种候选形态探索见 DESIGN-IT-TWICE.md并行设计若干激进不同的接口再按深度、局部性与缝的位置对比取舍。值得说明的是该技能目前位于仓库的in-progress/beta桶中根据 in-progress/README.md 的说明处于 beta 的技能不会进入插件与顶层 README可以按npx skillslatest add mattpocock/skills --skillsetup-ts-deep-modules的方式单独安装。其配套的 Agent 声明 agents/openai.yaml 将allow_implicit_invocation设为falsedisable-model-invocation: true表明它是用户主动调用的技能而非模型可自行触发的隐式技能。落地后你应该拥有什么完成七步之后仓库将获得四项可验证的成果一个可运行的边界检查lint:boundaries与 typecheck 同命令执行任何深导入、跨包触底、测试直取内部、依赖成环都会让 CI 红牌一个统一的包形态根目录入口 lib/实现 tests/测试任何新包都能照抄 example 模板一份面向未来的约定文档packages-root/README.md明确反对 barrel、倡导多入口一条 Agent 可发现的路径CLAUDE.md/AGENTS.md中的一行指针让后续所有编码 Agent 在动手前先读到边界规则而不是在违规报错后才被迫理解它。最终这套配置让深度优先从设计口号变成持续集成的硬约束接口即边界边界即 CICI 即文化。【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考