setup-ts-deep-modules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Setup 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
/codebase-design
skill — use its language throughout.
让仓库中的每个包都成为深度模块:用简洁的接口封装大量实现逻辑。包的公共接口是其入口文件——即包根目录下的文件——子文件夹中的所有内容均为内部实现,对外隐藏。本技能会安装dependency-cruiser并配置规则,确保只有入口文件可被外部访问,同时验证规则生效。
若想了解相关术语(深度模块、接口、接缝、深度),请运行
/codebase-design
技能——全程使用该技能定义的术语。

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).
The public surface is the package's root files — not one designated
index.ts
. By convention implementation lives in
lib/
and tests in
tests/
, 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.
Four rules, all
error
:
  1. 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.
  2. Intra-package freedom — a package's own files import each other freely.
  3. Tests through the entry points — files under
    <pkg>/tests/
    may import any package's entry points and their own
    tests/
    fixtures, but never any package's subfolder internals (not even their own). Integration tests across packages are fine; deep imports are not.
  4. 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 (
index.ts
,
client.ts
,
server.ts
) instead of funnelling everything through one giant
index.ts
. Barrel files that re-export a whole subtree are discouraged — keep entry points small and hide implementation in subfolders.
Layering (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.ts
。按照约定,实现代码放在
lib/
,测试用例放在
tests/
,让每个包都保持相同的双文件夹结构。不过规则本身是通用的:任何子文件夹中的内容都是私有的,因此无需扩展配置来新增文件夹。
四条规则,均设为
error
级别:
  1. 入口边界规则 —— 包外代码(应用代码或其他包)仅可导入该包的入口文件(根目录文件),绝不能导入其子文件夹中的内容。
  2. 包内自由规则 —— 包内文件可自由互相导入。
  3. 测试仅通过入口规则 ——
    <pkg>/tests/
    下的文件可导入任意包的入口文件及自身
    tests/
    中的测试数据,但绝不能导入任意包的子文件夹内部实现(即使是自身包的也不行)。跨包集成测试是允许的;深度导入则不允许。
  4. 无循环依赖规则 —— 禁止存在依赖循环。
使用入口文件而非桶文件。由于公共接口是所有根目录文件,包可以暴露多个小型入口文件(如
index.ts
client.ts
server.ts
),而非将所有内容通过一个庞大的
index.ts
导出。不推荐使用桶文件重新导出整个子树——保持入口文件简洁,将实现逻辑隐藏在子文件夹中。
分层(哪些包可以依赖哪些包)是另一个独立的关注点,已在本仓库的配置中留空注释,供后续填充。

Steps

步骤

1. Detect the environment

1. 检测环境

  • Package manager
    pnpm-lock.yaml
    → pnpm,
    yarn.lock
    → yarn,
    bun.lockb
    → bun, else npm. Use it for every command below (
    pnpm
    /
    yarn
    /
    npm run
    /
    bunx
    ).
  • Packages root — if
    src/
    exists use
    src/packages
    , else
    packages
    . Confirm the choice with the user if the repo already has a different obvious convention.
  • Existing config — check for a
    .dependency-cruiser.*
    file. If one exists, do not overwrite it: merge the four rules and the options in, and tell the user what you added.
Done when: package manager, packages root, and existing-config status are all known.
  • 包管理器 —— 若存在
    pnpm-lock.yaml
    则用pnpm,
    yarn.lock
    则用yarn,
    bun.lockb
    则用bun,否则用npm。后续所有命令都使用该包管理器(
    pnpm
    /
    yarn
    /
    npm run
    /
    bunx
    )。
  • 包根目录 —— 若存在
    src/
    则使用
    src/packages
    ,否则用
    packages
    。如果仓库已有明显不同的约定,请与用户确认选择。
  • 现有配置 —— 检查是否存在
    .dependency-cruiser.*
    文件。若已存在,不要覆盖:将四条规则和配置选项合并进去,并告知用户新增的内容。
完成标志:已确定包管理器、包根目录及现有配置状态。

2. Install dependency-cruiser

2. 安装dependency-cruiser

Install
dependency-cruiser
as a devDependency with the detected package manager.
Done when:
dependency-cruiser
is in
devDependencies
.
使用检测到的包管理器将
dependency-cruiser
安装为开发依赖。
完成标志
dependency-cruiser
已出现在
devDependencies
中。

3. Write the config

3. 编写配置文件

Copy
dependency-cruiser.config.cjs
to the repo root as
.dependency-cruiser.cjs
. Set
PACKAGES_ROOT
to the root detected in step 1. The rules are path-depth based and extension-agnostic, so nothing else needs adapting.
Done when:
.dependency-cruiser.cjs
exists with the correct
PACKAGES_ROOT
, and the four forbidden rules are present.
dependency-cruiser.config.cjs
复制到仓库根目录,命名为
.dependency-cruiser.cjs
。将
PACKAGES_ROOT
设置为步骤1中检测到的根目录。规则基于路径深度且与文件扩展名无关,因此无需调整其他内容。
完成标志
.dependency-cruiser.cjs
已存在,且
PACKAGES_ROOT
设置正确,四条禁止规则已包含在内。

4. Wire it into the checks

4. 集成到检查流程

  • Add a
    lint:boundaries
    script:
    depcruise <packages-root>
    (or
    depcruise src
    ).
  • Fold it into the repo's umbrella check command — the one that already runs typecheck (e.g. a
    check
    /
    ci
    /
    validate
    script). Do not touch
    tsconfig
    or add path aliases.
  • If there is no umbrella script, add
    lint:boundaries
    and tell the user to include it in CI.
Done when:
lint:boundaries
exists and runs as part of the same command as typecheck.
  • 添加
    lint:boundaries
    脚本:
    depcruise <packages-root>
    (或
    depcruise src
    )。
  • 将其集成到仓库的统一检查命令中——即已运行类型检查的命令(如
    check
    /
    ci
    /
    validate
    脚本)。不要修改
    tsconfig
    或添加路径别名。
  • 若不存在统一检查脚本,则添加
    lint:boundaries
    并告知用户将其纳入CI流程。
完成标志
lint:boundaries
已存在,且与类型检查命令一起运行。

5. Scaffold the example package

5. 搭建示例包

Create a committed
<packages-root>/example/
as a copy-me template:
  • index.ts
    — an entry point. Export one function that delegates to an internal file (so the package is visibly deep, not a pass-through).
  • lib/impl.ts
    — an internal file in a subfolder, imported by
    index.ts
    , not reachable from outside.
  • tests/example.test.ts
    — imports only
    ../index
    (an entry point), and asserts against the public function.
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
impl
in a subfolder.
创建已提交的
<packages-root>/example/
作为可复制的模板:
  • index.ts
    —— 入口文件。导出一个委托给内部文件的函数(使包明显是深度模块,而非简单的中转)。
  • lib/impl.ts
    —— 子文件夹中的内部文件,被
    index.ts
    导入,不可被外部访问。
  • tests/example.test.ts
    —— 仅导入
    ../index
    (入口文件),并针对公共函数进行断言。
告知用户这是可复制或删除的起始模板。
完成标志:示例包已创建,通过根目录入口文件暴露功能,并将
impl
隐藏在子文件夹中。

6. 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.
  1. Run
    lint:boundaries
    . It must pass on the clean example.
  2. Temporarily add a deep import to
    tests/example.test.ts
    (e.g.
    import { thing } from "../lib/impl"
    ). Run
    lint:boundaries
    again — it must fail with
    tests-through-entrypoints
    .
  3. 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.
这是整个技能的完成标准——无法检测违规的配置毫无价值。
  1. 运行
    lint:boundaries
    。在干净的示例包上必须通过
  2. 临时在
    tests/example.test.ts
    中添加深度导入(如
    import { thing } from "../lib/impl"
    )。再次运行
    lint:boundaries
    ——必须因
    tests-through-entrypoints
    规则失败
  3. 撤销深度导入。再次运行——必须通过
完成标志:已观察到一次通过、一次深度导入导致的失败、一次再次通过。若步骤2未失败,则说明规则未正确配置——修复后再完成操作。

7. Document the convention

7. 记录约定

Write a
README.md
in the packages folder (
<packages-root>/README.md
) — next to the packages it governs — covering: the
src/packages/<name>/
layout (entry points at the root,
lib/
for implementation,
tests/
for tests), "import only through a package's entry points (its root files)", and how to run
lint:boundaries
. 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.
Then add a context pointer to it from the repo's agent-instructions file —
CLAUDE.md
if present, else
AGENTS.md
(create
AGENTS.md
if neither exists). One line is enough, e.g.
Packages are deep modules — see [src/packages/README.md](./src/packages/README.md) before adding or importing one.
This is what makes an agent discover the boundary rule instead of tripping over it.
Done when:
<packages-root>/README.md
exists and discourages barrels, and the repo's
CLAUDE.md
/
AGENTS.md
links to it.
在包文件夹中(
<packages-root>/README.md
)编写
README.md
——与它所管理的包同级——内容包括:
src/packages/<name>/
的结构(入口文件在根目录,
lib/
存放实现代码,
tests/
存放测试用例)、“仅通过包的入口文件(根目录文件)导入”,以及如何运行
lint:boundaries
明确不推荐使用桶文件——应暴露多个小型入口文件,而非通过单个index重新导出整个子树。内容控制在可复制的模板片段加上四条规则的简短说明即可。
然后在仓库的代理说明文件中添加上下文指向——若存在
CLAUDE.md
则用它,否则用
AGENTS.md
(若两者都不存在则创建
AGENTS.md
)。只需一行内容,例如
包为深度模块——添加或导入包前请查看[src/packages/README.md](./src/packages/README.md)。
这能让代理发现边界规则,而非违反规则。
完成标志
<packages-root>/README.md
已存在且明确不推荐桶文件,仓库的
CLAUDE.md
/
AGENTS.md
已链接到该文件。

Notes

注意事项

  • The config's
    $1
    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.
  • 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
    lib/
    (implementation) and
    tests/
    , 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.
  • 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
    .cjs
    (not
    .js
    ) so the config's
    module.exports
    works even in
    "type": "module"
    repos.
  • 配置中的
    $1
    反向引用(dependency-cruiser的组匹配)是实现包内可访问自身内部代码而外部不可访问的关键——不要将其拆分为单独的每个包规则。
  • 公共与私有由路径深度决定:包的根目录文件是入口文件;任何子文件夹中的内容都是私有的。约定的子文件夹是
    lib/
    (实现代码)和
    tests/
    ,但规则并未硬编码这些名称——任何子文件夹都是私有的,因此新增文件夹无需修改配置。添加入口文件只需在根目录新增文件——无需桶文件。
  • 包结构为扁平:直接位于根目录下的一级子目录。包的内部实现可任意嵌套;包中不能包含其他包。
  • 使用
    .cjs
    (而非
    .js
    ),这样即使在
    "type": "module"
    的仓库中,配置的
    module.exports
    也能正常工作。