setup-ts-deep-modules
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSetup TS Deep Modules
配置TS深度模块
Make every package in this repo a deep module: a lot of behaviour behind a small interface. A package's public surface is its entry points — the files at the package root — and everything in its subfolders is hidden. This skill installs dependency-cruiser and the rules that make the entry points the only way in, then proves the rules bite.
For the vocabulary (deep module, interface, seam, depth), run the skill — use its language throughout.
/codebase-design让仓库中的每个包都成为深度模块:用简洁的接口封装大量实现逻辑。包的公共接口是其入口文件——即包根目录下的文件——子文件夹中的所有内容均为内部实现,对外隐藏。本技能会安装dependency-cruiser并配置规则,确保只有入口文件可被外部访问,同时验证规则生效。
若想了解相关术语(深度模块、接口、接缝、深度),请运行技能——全程使用该技能定义的术语。
/codebase-designThe shape this enforces
强制执行的结构
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).The public surface is the package's root files — not one designated . By convention implementation lives in and tests in , giving every package the same two-folder shape. The rule itself is general, though: anything in any subfolder is private, so you never extend the config to add a folder.
index.tslib/tests/Four rules, all :
error- Entry-point boundary — code outside a package (app code or another package) may import only that package's entry points (its root files), never anything in its subfolders.
- Intra-package freedom — a package's own files import each other freely.
- Tests through the entry points — files under may import any package's entry points and their own
<pkg>/tests/fixtures, but never any package's subfolder internals (not even their own). Integration tests across packages are fine; deep imports are not.tests/ - No cycles — no dependency cycles.
Entry points, not a barrel. Because the public surface is every root file, a package can expose several small entry points (, , ) instead of funnelling everything through one giant . Barrel files that re-export a whole subtree are discouraged — keep entry points small and hide implementation in subfolders.
index.tsclient.tsserver.tsindex.tsLayering (which packages may depend on which) is a different concern and is left as a commented stub in the config for this repo to fill in.
src/packages/
<name>/
index.ts ← 入口文件(公共)。外部代码从此导入。
client.ts ← 另一个入口文件。包可以暴露多个入口。
lib/ ← 实现代码:对外隐藏,内部文件可自由互相导入。
tests/ ← 同目录测试用例 + 测试数据(子文件夹,属于私有内容)。公共接口是包的根目录文件——而非指定单个。按照约定,实现代码放在,测试用例放在,让每个包都保持相同的双文件夹结构。不过规则本身是通用的:任何子文件夹中的内容都是私有的,因此无需扩展配置来新增文件夹。
index.tslib/tests/四条规则,均设为级别:
error- 入口边界规则 —— 包外代码(应用代码或其他包)仅可导入该包的入口文件(根目录文件),绝不能导入其子文件夹中的内容。
- 包内自由规则 —— 包内文件可自由互相导入。
- 测试仅通过入口规则 —— 下的文件可导入任意包的入口文件及自身
<pkg>/tests/中的测试数据,但绝不能导入任意包的子文件夹内部实现(即使是自身包的也不行)。跨包集成测试是允许的;深度导入则不允许。tests/ - 无循环依赖规则 —— 禁止存在依赖循环。
使用入口文件而非桶文件。由于公共接口是所有根目录文件,包可以暴露多个小型入口文件(如、、),而非将所有内容通过一个庞大的导出。不推荐使用桶文件重新导出整个子树——保持入口文件简洁,将实现逻辑隐藏在子文件夹中。
index.tsclient.tsserver.tsindex.ts分层(哪些包可以依赖哪些包)是另一个独立的关注点,已在本仓库的配置中留空注释,供后续填充。
Steps
步骤
1. Detect the environment
1. 检测环境
- Package manager — → pnpm,
pnpm-lock.yaml→ yarn,yarn.lock→ bun, else npm. Use it for every command below (bun.lockb/pnpm/yarn/npm run).bunx - Packages root — if exists use
src/, elsesrc/packages. Confirm the choice with the user if the repo already has a different obvious convention.packages - Existing config — check for a file. If one exists, do not overwrite it: merge the four rules and the options in, and tell the user what you added.
.dependency-cruiser.*
Done when: package manager, packages root, and existing-config status are all known.
- 包管理器 —— 若存在则用pnpm,
pnpm-lock.yaml则用yarn,yarn.lock则用bun,否则用npm。后续所有命令都使用该包管理器(bun.lockb/pnpm/yarn/npm run)。bunx - 包根目录 —— 若存在则使用
src/,否则用src/packages。如果仓库已有明显不同的约定,请与用户确认选择。packages - 现有配置 —— 检查是否存在文件。若已存在,不要覆盖:将四条规则和配置选项合并进去,并告知用户新增的内容。
.dependency-cruiser.*
完成标志:已确定包管理器、包根目录及现有配置状态。
2. Install dependency-cruiser
2. 安装dependency-cruiser
Install as a devDependency with the detected package manager.
dependency-cruiserDone when: is in .
dependency-cruiserdevDependencies使用检测到的包管理器将安装为开发依赖。
dependency-cruiser完成标志:已出现在中。
dependency-cruiserdevDependencies3. Write the config
3. 编写配置文件
Copy to the repo root as . Set to the root detected in step 1. The rules are path-depth based and extension-agnostic, so nothing else needs adapting.
dependency-cruiser.config.cjs.dependency-cruiser.cjsPACKAGES_ROOTDone when: exists with the correct , and the four forbidden rules are present.
.dependency-cruiser.cjsPACKAGES_ROOT将复制到仓库根目录,命名为。将设置为步骤1中检测到的根目录。规则基于路径深度且与文件扩展名无关,因此无需调整其他内容。
dependency-cruiser.config.cjs.dependency-cruiser.cjsPACKAGES_ROOT完成标志:已存在,且设置正确,四条禁止规则已包含在内。
.dependency-cruiser.cjsPACKAGES_ROOT4. Wire it into the checks
4. 集成到检查流程
- Add a script:
lint:boundaries(ordepcruise <packages-root>).depcruise src - Fold it into the repo's umbrella check command — the one that already runs typecheck (e.g. a /
check/ciscript). Do not touchvalidateor add path aliases.tsconfig - If there is no umbrella script, add and tell the user to include it in CI.
lint:boundaries
Done when: exists and runs as part of the same command as typecheck.
lint:boundaries- 添加脚本:
lint:boundaries(或depcruise <packages-root>)。depcruise src - 将其集成到仓库的统一检查命令中——即已运行类型检查的命令(如/
check/ci脚本)。不要修改validate或添加路径别名。tsconfig - 若不存在统一检查脚本,则添加并告知用户将其纳入CI流程。
lint:boundaries
完成标志:已存在,且与类型检查命令一起运行。
lint:boundaries5. Scaffold the example package
5. 搭建示例包
Create a committed as a copy-me template:
<packages-root>/example/- — an entry point. Export one function that delegates to an internal file (so the package is visibly deep, not a pass-through).
index.ts - — an internal file in a subfolder, imported by
lib/impl.ts, not reachable from outside.index.ts - — imports only
tests/example.test.ts(an entry point), and asserts against the public function.../index
Tell the user this is a starter template to copy or delete.
Done when: the example package exists, exposes its behaviour through a root entry point, and hides in a subfolder.
impl创建已提交的作为可复制的模板:
<packages-root>/example/- —— 入口文件。导出一个委托给内部文件的函数(使包明显是深度模块,而非简单的中转)。
index.ts - —— 子文件夹中的内部文件,被
lib/impl.ts导入,不可被外部访问。index.ts - —— 仅导入
tests/example.test.ts(入口文件),并针对公共函数进行断言。../index
告知用户这是可复制或删除的起始模板。
完成标志:示例包已创建,通过根目录入口文件暴露功能,并将隐藏在子文件夹中。
impl6. Prove the rules bite
6. 验证规则生效
This is the completion criterion for the whole skill — a config that doesn't fail on a violation is worthless.
- Run . It must pass on the clean example.
lint:boundaries - Temporarily add a deep import to (e.g.
tests/example.test.ts). Runimport { thing } from "../lib/impl"again — it must fail withlint:boundaries.tests-through-entrypoints - Revert the deep import. Run once more — it must pass.
Done when: you have observed a pass, then a fail on the deep import, then a pass again. If step 2 does not fail, the rules are not wired correctly — fix before finishing.
这是整个技能的完成标准——无法检测违规的配置毫无价值。
- 运行。在干净的示例包上必须通过。
lint:boundaries - 临时在中添加深度导入(如
tests/example.test.ts)。再次运行import { thing } from "../lib/impl"——必须因lint:boundaries规则失败。tests-through-entrypoints - 撤销深度导入。再次运行——必须通过。
完成标志:已观察到一次通过、一次深度导入导致的失败、一次再次通过。若步骤2未失败,则说明规则未正确配置——修复后再完成操作。
7. Document the convention
7. 记录约定
Write a in the packages folder () — next to the packages it governs — covering: the layout (entry points at the root, for implementation, for tests), "import only through a package's entry points (its root files)", and how to run . Discourage barrel files explicitly — expose several small entry points instead of re-exporting a whole subtree through one index. Keep it to the copy-me snippet plus the four rules in one paragraph each.
README.md<packages-root>/README.mdsrc/packages/<name>/lib/tests/lint:boundariesThen add a context pointer to it from the repo's agent-instructions file — if present, else (create if neither exists). One line is enough, e.g. This is what makes an agent discover the boundary rule instead of tripping over it.
CLAUDE.mdAGENTS.mdAGENTS.mdPackages are deep modules — see [src/packages/README.md](./src/packages/README.md) before adding or importing one.Done when: exists and discourages barrels, and the repo's / links to it.
<packages-root>/README.mdCLAUDE.mdAGENTS.md在包文件夹中()编写——与它所管理的包同级——内容包括:的结构(入口文件在根目录,存放实现代码,存放测试用例)、“仅通过包的入口文件(根目录文件)导入”,以及如何运行。明确不推荐使用桶文件——应暴露多个小型入口文件,而非通过单个index重新导出整个子树。内容控制在可复制的模板片段加上四条规则的简短说明即可。
<packages-root>/README.mdREADME.mdsrc/packages/<name>/lib/tests/lint:boundaries然后在仓库的代理说明文件中添加上下文指向——若存在则用它,否则用(若两者都不存在则创建)。只需一行内容,例如 这能让代理发现边界规则,而非违反规则。
CLAUDE.mdAGENTS.mdAGENTS.md包为深度模块——添加或导入包前请查看[src/packages/README.md](./src/packages/README.md)。完成标志:已存在且明确不推荐桶文件,仓库的/已链接到该文件。
<packages-root>/README.mdCLAUDE.mdAGENTS.mdNotes
注意事项
- The config's back-references (dependency-cruiser's group matching) are what let a package reach its own internals while outsiders can't — don't flatten them into separate per-package rules.
$1 - Public vs private is decided by depth: a package's root files are entry points; anything in a subfolder is private. The conventional subfolders are (implementation) and
lib/, but the rule doesn't hardcode them — any subfolder is private, so a new folder never needs a config change. Adding an entry point is just adding a root file — no barrel.tests/ - Packages are flat: one tier of immediate children under the root. A package's internals may nest as deep as you like; a package may not contain another package.
- Use (not
.cjs) so the config's.jsworks even inmodule.exportsrepos."type": "module"
- 配置中的反向引用(dependency-cruiser的组匹配)是实现包内可访问自身内部代码而外部不可访问的关键——不要将其拆分为单独的每个包规则。
$1 - 公共与私有由路径深度决定:包的根目录文件是入口文件;任何子文件夹中的内容都是私有的。约定的子文件夹是(实现代码)和
lib/,但规则并未硬编码这些名称——任何子文件夹都是私有的,因此新增文件夹无需修改配置。添加入口文件只需在根目录新增文件——无需桶文件。tests/ - 包结构为扁平:直接位于根目录下的一级子目录。包的内部实现可任意嵌套;包中不能包含其他包。
- 使用(而非
.cjs),这样即使在.js的仓库中,配置的"type": "module"也能正常工作。module.exports