setup-ts-deep-modules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Setup TS Deep Modules

配置 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)来自
/codebase-design
skill;整个过程都使用它的语言。
让 repo 中的每个 package 成为 deep module:用小型 interface 隐藏大量 behaviour。Package 的 public surface 是其 entry points(package root 中的文件),所有 subfolders 均为隐藏状态。本 Skill 会安装 dependency-cruiser,添加强制要求仅通过 entry points 访问的 rules,并验证这些 rules 确实能够拦截违规操作。
术语(deep module、interface、seam、depth)源自
/codebase-design
Skill;整个流程均使用该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,并非指定的单个
index.ts
。按 convention,implementation 放在
lib/
,tests 放在
tests/
,使所有 packages 采用相同的 two-folder shape。Rule 本身是通用的:任何 subfolder 中的任何内容都是 private,因此永远无需为了新增 folder 扩展 config。
四条 rules,全部为
error
  1. Entry-point boundary — package 外的 code(app code 或其他 package)只能 import 该 package 的 entry points(root files),不能 import subfolder 中的任何内容。
  2. Intra-package freedom — package 自己的 files 可以自由互相 import。
  3. Tests through the entry points
    <pkg>/tests/
    下的 files 可以 import 任意 package 的 entry points 和自己的
    tests/
    fixtures,但不能 import 任何 package 的 subfolder internals(包括自己的)。允许跨 package integration tests,不允许 deep imports。
  4. No cycles — 不允许 dependency cycles。
Entry points, not a barrel. Public surface 是每个 root file,因此 package 可以提供多个小 entry points(
index.ts
client.ts
server.ts
),不必把一切汇入巨大的
index.ts
。不鼓励 re-export 整个 subtree 的 barrel files;entry points 要小,implementation 隐藏在 subfolders。
Layering(哪些 packages 可以依赖哪些)是另一个 concern,在 config 中保留 commented stub,由当前 repo 填写。
src/packages/
  <name>/
    index.ts        ← 入口文件(公开)。从外部导入此文件。
    client.ts       ← 另一个入口文件。Package 可以暴露多个入口。
    lib/            ← 实现代码:对外部隐藏,内部可自由互相导入。
    tests/          ← 同目录测试用例 + 测试数据(子目录,因此为私有)。
Public surface 是 package 的 root files,而非指定的单个
index.ts
。按照约定,implementation 放在
lib/
目录下,tests 放在
tests/
目录下,让所有 packages 采用统一的双目录结构。Rule 本身具备通用性:任何 subfolder 中的任何内容均为 private,因此新增目录时永远无需扩展配置。
四条规则,全部设为
error
级别:
  1. Entry-point boundary — package 外部的代码(应用代码或其他 package)只能导入该 package 的 entry points(root files),不得导入 subfolder 中的任何内容。
  2. Intra-package freedom — package 内部的文件可自由互相导入。
  3. Tests through the entry points
    <pkg>/tests/
    目录下的文件可导入任意 package 的 entry points 以及自身
    tests/
    目录下的 fixtures,但不得导入任何 package 的 subfolder internals(包括自身的)。允许跨 package 集成测试,禁止深度导入。
  4. No cycles — 禁止依赖循环。
入口文件,而非桶文件。 Public surface 是每个 root file,因此 package 可以提供多个小型 entry points(
index.ts
client.ts
server.ts
),无需将所有内容汇总到一个庞大的
index.ts
中。不鼓励使用 barrel files 重新导出整个子目录;entry points 应保持精简,implementation 需隐藏在 subfolders 中。
分层(即哪些 packages 可依赖哪些 packages)是另一项需要关注的内容,配置中保留了注释占位符,由当前 repo 自行填充。

Steps

步骤

1. Detect the environment

1. 检测环境

  • Package manager
    pnpm-lock.yaml
    → pnpm,
    yarn.lock
    → yarn,
    bun.lockb
    → bun,否则 npm。后续每条 command 都使用它。
  • Packages root — 存在
    src/
    就用
    src/packages
    ,否则用
    packages
    。如果 repo 已有明显不同的 convention,与用户确认。
  • Existing config — 检查
    .dependency-cruiser.*
    file。若存在,不要覆盖;merge 四条 rules 和 options,并说明添加了什么。
Done when: package manager、packages root 和 existing-config status 全部明确。
  • 包管理器 — 存在
    pnpm-lock.yaml
    则使用 pnpm,
    yarn.lock
    则使用 yarn,
    bun.lockb
    则使用 bun,否则使用 npm。后续所有命令均使用该包管理器。
  • Packages 根目录 — 若存在
    src/
    目录则使用
    src/packages
    ,否则使用
    packages
    。如果 repo 已有明显不同的约定,需与用户确认。
  • 现有配置 — 检查
    .dependency-cruiser.*
    文件。若存在,则不覆盖;而是合并四条 rules 和配置项,并说明新增内容。
完成标志: 包管理器、packages 根目录和现有配置状态均已明确。

2. Install dependency-cruiser

2. 安装 dependency-cruiser

使用检测到的 package manager,把
dependency-cruiser
安装为 devDependency。
Done when:
dependency-cruiser
出现在
devDependencies
使用检测到的包管理器,将
dependency-cruiser
安装为 devDependency。
完成标志:
dependency-cruiser
出现在
devDependencies
中。

3. Write the config

3. 编写配置文件

dependency-cruiser.config.cjs
复制到 repo root,命名为
.dependency-cruiser.cjs
。把
PACKAGES_ROOT
设置成 step 1 检测到的 root。Rules 基于 path depth 且与 extension 无关,不需要其他调整。
Done when:
.dependency-cruiser.cjs
存在、
PACKAGES_ROOT
正确,并包含四条 forbidden rules。
dependency-cruiser.config.cjs
复制到 repo 根目录,命名为
.dependency-cruiser.cjs
。将
PACKAGES_ROOT
设置为步骤1中检测到的根目录。规则基于路径深度且与文件扩展名无关,无需其他调整。
完成标志:
.dependency-cruiser.cjs
文件存在、
PACKAGES_ROOT
配置正确,且包含四条禁止性规则。

4. Wire it into the checks

4. 集成到检查流程

  • 添加
    lint:boundaries
    script:
    depcruise <packages-root>
    (或
    depcruise src
    )。
  • 把它纳入 repo 已经执行 typecheck 的 umbrella check command(如
    check
    /
    ci
    /
    validate
    )。不要修改
    tsconfig
    或添加 path aliases。
  • 如果没有 umbrella script,就添加
    lint:boundaries
    ,并告诉用户把它加入 CI。
Done when:
lint:boundaries
存在,且和 typecheck 由同一 command 运行。
  • 添加
    lint:boundaries
    脚本:
    depcruise <packages-root>
    (或
    depcruise src
    )。
  • 将其纳入 repo 已有的类型检查总览命令(如
    check
    /
    ci
    /
    validate
    )。不要修改
    tsconfig
    或添加路径别名。
  • 如果没有总览脚本,则添加
    lint:boundaries
    ,并告知用户将其加入 CI 流程。
完成标志:
lint:boundaries
脚本存在,且与类型检查通过同一命令执行。

5. Scaffold the example package

5. 搭建示例 Package

创建并 commit 一个
<packages-root>/example/
作为 copy-me template:
  • index.ts
    — entry point,export 一个 delegate 给 internal file 的 function,让 package 明显是 deep,不是 pass-through。
  • lib/impl.ts
    subfolder 中的 internal file,由
    index.ts
    import,外部无法访问。
  • tests/example.test.ts
    import
    ../index
    (entry point),并针对 public function assert。
告诉用户这是可以 copy 或 delete 的 starter template。
Done when: example package 存在,通过 root entry point 暴露 behaviour,并把
impl
隐藏在 subfolder。
创建并提交一个
<packages-root>/example/
作为可复制的模板:
  • index.ts
    — 入口文件,导出一个委托给内部文件的函数,明确该 package 是 deep 类型,而非透传类型。
  • lib/impl.ts
    子目录 中的内部文件,由
    index.ts
    导入,外部无法访问。
  • tests/example.test.ts
    导入
    ../index
    (入口文件),并针对公开函数进行断言。
告知用户这是可复制或删除的起始模板。
完成标志: 示例 package 存在,通过根入口文件暴露功能,并将
impl
隐藏在子目录中。

6. Prove the rules bite

6. 验证规则有效性

这是整个 skill 的 completion criterion;不能在 violation 时失败的 config 毫无价值。
  1. 运行
    lint:boundaries
    ,clean example 必须 pass
  2. 临时给
    tests/example.test.ts
    加一个 deep import,例如
    import { thing } from "../lib/impl"
    。再次运行
    lint:boundaries
    ,必须以
    tests-through-entrypoints
    fail
  3. Revert deep import,再运行一次,必须 pass
Done when: 已观察到 pass、deep import 时 fail、恢复后再 pass。Step 2 不失败,就先修正 wiring,不能完成任务。
这是本 Skill 的完成标准;无法拦截违规的配置毫无价值。
  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. 记录约定规则

在 packages folder(
<packages-root>/README.md
)中写
README.md
,内容覆盖:
src/packages/<name>/
layout(root 中是 entry points、
lib/
放 implementation、
tests/
放 tests)、“只通过 package 的 entry points(root files)import”,以及如何运行
lint:boundaries
。明确 discourage barrel files,用多个小 entry points,而不是从一个 index re-export 整个 subtree。内容只保留 copy-me snippet,以及四条 rules 各一段。
再从 repo 的 agent-instructions file 指向它:优先
CLAUDE.md
,否则
AGENTS.md
;两者都不存在则创建
AGENTS.md
。一行即可,例如:
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 的
CLAUDE.md
/
AGENTS.md
链接到它。
在 packages 目录(
<packages-root>/README.md
)中编写
README.md
,内容涵盖:
src/packages/<name>/
的目录结构(根目录为入口文件、
lib/
存放实现代码、
tests/
存放测试用例)、“仅通过 package 的入口文件(根目录文件)导入”的规则,以及如何运行
lint:boundaries
。明确 不鼓励使用桶文件,应使用多个小型入口文件,而非从单个 index 文件重新导出整个子目录。内容仅保留可复制的代码片段,以及四条规则的简要说明。
再从 repo 的 agent 指令文件指向该文档:优先选择
CLAUDE.md
,否则选择
AGENTS.md
;若两者均不存在则创建
AGENTS.md
。只需一行内容,例如:
Packages 为 deep modules — 添加或导入前请查看 [src/packages/README.md](./src/packages/README.md)。
这样 agent 就能发现边界规则,而非触发违规。
完成标志:
<packages-root>/README.md
存在且明确不鼓励使用桶文件,repo 的
CLAUDE.md
/
AGENTS.md
已链接到该文档。

Notes

注意事项

  • Config 中的
    $1
    back-references(dependency-cruiser group matching)让 package 能访问自己的 internals,同时阻止 outsiders;不要把它们展开成每 package 一条 rule。
  • Public/private 由 depth 决定:package root files 是 entry points,subfolder 中的一切都是 private。Convention 是
    lib/
    tests/
    ,但 rule 不 hardcode;新增 folder 无需改 config,新增 entry point 只需新增 root file,不需要 barrel。
  • Packages 是 flat:root 下只有一层 immediate children。Package internals 可以任意深,但 package 不能包含另一个 package。
  • 使用
    .cjs
    (不是
    .js
    ),确保即使 repo 使用
    "type": "module"
    ,config 的
    module.exports
    也能工作。
  • 配置中的
    $1
    反向引用(dependency-cruiser 的分组匹配)允许 package 访问自身内部代码,同时阻止外部访问;请勿将其展开为每个 package 一条规则。
  • 公开/私有由 路径深度 决定:package 根目录文件为入口文件,子目录中的所有内容均为私有。约定使用
    lib/
    tests/
    目录,但规则未硬编码;新增目录无需修改配置,新增入口文件只需在根目录添加文件,无需使用桶文件。
  • Packages 采用 扁平结构:根目录下仅包含一级直接子目录。Package 内部可任意嵌套,但 package 不能包含另一个 package。
  • 使用
    .cjs
    格式(而非
    .js
    ),确保即使 repo 使用
    "type": "module"
    ,配置中的
    module.exports
    仍能正常工作。