setup-ts-deep-modules

作者: mattpocock

将 dependency-cruiser 接入 TypeScript 仓库,使每个包成为深度模块——实现隐藏在子文件夹中,仅能通过入口文件访问。由用户调用。

npx skills add https://github.com/mattpocock/skills --skill setup-ts-deep-modules

Setup TS Deep Modules

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.

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.

Steps

1. Detect the environment

  • Package managerpnpm-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.

2. Install dependency-cruiser

Install dependency-cruiser as a devDependency with the detected package manager.

Done when: dependency-cruiser is in devDependencies.

3. Write the config

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.

4. Wire it into the checks

  • 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.

5. Scaffold the example package

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.

6. Prove the rules bite

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.

7. Document the convention

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.

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.

来自 mattpocock 的更多技能

improve-codebase-architecture
mattpocock
在代码库中发现深层优化机会,依据CONTEXT.md中的领域语言和docs/adr/中的决策。适用于用户希望改进架构、寻找重构机会、整合紧耦合模块,或使代码库更易于测试和AI导航时使用。
developmentcode-reviewapi
tdd
mattpocock
采用红绿重构循环的测试驱动开发。当用户希望使用TDD构建功能或修复缺陷、提及“红绿重构”、需要集成测试或要求测试优先开发时使用。
developmenttesting
handoff
mattpocock
将当前对话压缩为交接文档,供其他代理接手处理。
communicationproject-managementdocument
prototype
mattpocock
在投入正式开发前,先构建一个可丢弃的原型来完善设计。在两条分支间切换——一条是可运行的终端应用,用于验证状态/业务逻辑问题;另一条是多个截然不同的UI变体,可通过单一路由切换。适用于用户希望进行原型设计、验证数据模型或状态机、制作UI草稿、探索设计方案,或提出“把这个做成原型”、“让我试试”、“尝试几种设计”等需求时使用。
developmentdesigncreative
triage
mattpocock
通过由分类角色驱动的状态机对问题进行分类。当用户想要创建问题、分类问题、审查传入的缺陷或功能请求、为AFK代理准备问题或管理工作流程时使用。
developmentproject-managementcommunication
obsidian-vault
mattpocock
在Obsidian vault中搜索、创建和管理笔记,支持维基链接和索引笔记。当用户想要在Obsidian中查找、创建或整理笔记时使用。
productivitydocument
edit-article
mattpocock
通过重组章节、提升清晰度及精简文笔来编辑和改进文章。适用于用户希望编辑、修订或优化文章草稿的场景。
documentcreative
writing-great-skills
mattpocock
关于如何写好和编辑技能的参考——使技能可预测的词汇和原则。
documentdevelopment