Setup TS Deep Modules
让 repo 中每个 package 成为
deep module:用小 interface 隐藏大量 behaviour。Package 的 public surface 是其
entry points(package root 中的 files),所有 subfolders 都隐藏。这个 skill 会安装
dependency-cruiser,加入强制只能通过 entry points 访问的 rules,并证明这些 rules 确实会拦截违规。
Vocabulary(deep module、interface、seam、depth)来自
skill;整个过程都使用它的语言。
The 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).
Public surface 是 package 的
root files,并非指定的单个
。按 convention,implementation 放在
,tests 放在
,使所有 packages 采用相同的 two-folder shape。Rule 本身是通用的:
任何 subfolder 中的
任何内容都是 private,因此永远无需为了新增 folder 扩展 config。
- Entry-point boundary — package 外的 code(app code 或其他 package)只能 import 该 package 的 entry points(root files),不能 import subfolder 中的任何内容。
- Intra-package freedom — package 自己的 files 可以自由互相 import。
- Tests through the entry points — 下的 files 可以 import 任意 package 的 entry points 和自己的 fixtures,但不能 import 任何 package 的 subfolder internals(包括自己的)。允许跨 package integration tests,不允许 deep imports。
- No cycles — 不允许 dependency cycles。
Entry points, not a barrel. Public surface 是每个 root file,因此 package 可以提供多个小 entry points(
、
、
),不必把一切汇入巨大的
。不鼓励 re-export 整个 subtree 的 barrel files;entry points 要小,implementation 隐藏在 subfolders。
Layering(哪些 packages 可以依赖哪些)是另一个 concern,在 config 中保留 commented stub,由当前 repo 填写。
Steps
1. Detect the environment
- Package manager — → pnpm, → yarn, → bun,否则 npm。后续每条 command 都使用它。
- Packages root — 存在 就用 ,否则用 。如果 repo 已有明显不同的 convention,与用户确认。
- Existing config — 检查 file。若存在,不要覆盖;merge 四条 rules 和 options,并说明添加了什么。
Done when: package manager、packages root 和 existing-config status 全部明确。
2. Install dependency-cruiser
使用检测到的 package manager,把
安装为 devDependency。
3. Write the config
把
dependency-cruiser.config.cjs
复制到 repo root,命名为
。把
设置成 step 1 检测到的 root。Rules 基于 path depth 且与 extension 无关,不需要其他调整。
Done when: 存在、
正确,并包含四条 forbidden rules。
4. Wire it into the checks
- 添加 script:
depcruise <packages-root>
(或 )。
- 把它纳入 repo 已经执行 typecheck 的 umbrella check command(如 / / )。不要修改 或添加 path aliases。
- 如果没有 umbrella script,就添加 ,并告诉用户把它加入 CI。
Done when: 存在,且和 typecheck 由同一 command 运行。
5. Scaffold the example package
创建并 commit 一个
作为 copy-me template:
- — entry point,export 一个 delegate 给 internal file 的 function,让 package 明显是 deep,不是 pass-through。
- — subfolder 中的 internal file,由 import,外部无法访问。
- — 只 import (entry point),并针对 public function assert。
告诉用户这是可以 copy 或 delete 的 starter template。
Done when: example package 存在,通过 root entry point 暴露 behaviour,并把
隐藏在 subfolder。
6. Prove the rules bite
这是整个 skill 的 completion criterion;不能在 violation 时失败的 config 毫无价值。
- 运行 ,clean example 必须 pass。
- 临时给 加一个 deep import,例如
import { thing } from "../lib/impl"
。再次运行 ,必须以 tests-through-entrypoints
fail。
- Revert deep import,再运行一次,必须 pass。
Done when: 已观察到 pass、deep import 时 fail、恢复后再 pass。Step 2 不失败,就先修正 wiring,不能完成任务。
7. Document the convention
在 packages folder(
<packages-root>/README.md
)中写
,内容覆盖:
layout(root 中是 entry points、
放 implementation、
放 tests)、“只通过 package 的 entry points(root files)import”,以及如何运行
。明确
discourage barrel files,用多个小 entry points,而不是从一个 index re-export 整个 subtree。内容只保留 copy-me snippet,以及四条 rules 各一段。
再从 repo 的 agent-instructions file 指向它:优先
,否则
;两者都不存在则创建
。一行即可,例如:
Packages are deep modules — see [src/packages/README.md](./src/packages/README.md) before adding or importing one.
这让 agent 能发现 boundary rule,而不是撞上它。
Done when: <packages-root>/README.md
存在且 discourages barrels,repo 的
/
链接到它。
Notes
- Config 中的 back-references(dependency-cruiser group matching)让 package 能访问自己的 internals,同时阻止 outsiders;不要把它们展开成每 package 一条 rule。
- Public/private 由 depth 决定:package root files 是 entry points,subfolder 中的一切都是 private。Convention 是 和 ,但 rule 不 hardcode;新增 folder 无需改 config,新增 entry point 只需新增 root file,不需要 barrel。
- Packages 是 flat:root 下只有一层 immediate children。Package internals 可以任意深,但 package 不能包含另一个 package。
- 使用 (不是 ),确保即使 repo 使用 ,config 的 也能工作。