Make every package in this repository adeep module: a large amount of behavior hidden behind a small interface. A package's public surface is itsentry point: 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 point the only path in, then verifies that the rules actually work.
For related vocabulary (deep module, interface, seam, depth), invoke the Skill tool with "codebase-design": use its terminology in what follows.
The structure it enforces
src/packages/
<name>/
index.ts ← 一个入口点(公开)。外部从这里导入。
client.ts ← 另一个入口点。Package 可以暴露多个。
lib/ ← 实现:对外部隐藏,内部可以自由相互导入。
tests/ ← 与实现并列的测试与 fixtures(一个子文件夹,所以是私有的)。
The public surface is the package'sroot files, not some designated index.ts. By convention the implementation goes in lib/, tests go in tests/, so that every package has the same "two subfolders" structure. The rules themselves, however, are universal:anyfile in aanysubfolder is private, so you never need to extend the config to add a new folder.
Four rules, all error:
- Entry point boundary: code outside a package (application code or another package) may only import that package's entry point (i.e. its root files), and must never import anything in its subfolders.
- Freedom within a package: a package's own files may freely import one another.
- Tests go through entry points:
<pkg>/tests/files under may import any package's entry point as well as its owntests/fixtures, but must not import the internals of any package's subfolders (including its own). Cross-package integration tests are fine, but deep-path imports are not. - No circular dependencies: dependency cycles are not allowed.
Entry points, not barrel files. Since the public surface iseachroot file, a package can expose multiple small entry points (index.ts、client.ts、server.ts), instead of cramming everything into one giant index.ts. Do not use barrel files to re-export an entire subtree; keep entry points small and focused, and hide the implementation in subfolders.
Layering (which packages may depend on which packages) isa separate matter, and this repository leaves it in the config as a commented stub for you to fill in.
Process
1. Probe the environment
- Package manager:
pnpm-lock.yaml→ pnpm;yarn.lock→ yarn;bun.lockb→ bun; otherwise npm. All subsequent commands use that package manager (pnpm/yarn/npm run/bunx)。 - packages root: if
src/exists, usesrc/packages, otherwise usepackages. If the repository already has a clearly different convention, confirm with the user. - Existing config: check whether
.dependency-cruiser.*files exist. If so,do not overwrite: merge the four rules and options into it, and tell the user what you added.
**Completion condition:** The package manager, the packages root directory, and whether existing configuration exists: all three have been confirmed.
2. Install dependency-cruiser
Using the package manager detected above, install dependency-cruiser as a devDependency.
Completion condition:dependency-cruiser has appeared in devDependencies .
3. Write the configuration
Copy dependency-cruiser.config.cjs to the repository root and name it .dependency-cruiser.cjs. Set PACKAGES_ROOT to the root directory detected in step 1. The rules are based on path depth and are independent of extensions, so no other adaptation is needed.
Completion condition:.dependency-cruiser.cjs exists,PACKAGES_ROOT is correct, and all four prohibition rules have been written.
4. Wire it into the check flow
- Add
lint:boundariesscript:depcruise <packages-root>(ordepcruise src)。 - merge it into the repository's "total check" command: the command that already runs typecheck (for example, the
check/ci/validatescript).Do not modifytsconfig, and do not add path aliases. - If there is no total check script, add
lint:boundariesand tell the user to include it in CI.
Completion condition:lint:boundaries already exists and runs as part of the same command as typecheck.
5. Set up an example package
Create a <packages-root>/example/that will be committed, as a "copy-and-use" template:
index.tsis an entry point. Export a function that delegates to an internal file (so the package looksdeep, rather than being a simple passthrough).lib/impl.ts: an internal file located in thesubfolderthat is imported byindex.tsand cannot be reached from outside.tests/example.test.tsonly imports../index(an entry point), and asserts against the public function.
Tell the user this is a starter template that can be copied or deleted.
**Completion condition:** The example package already exists, exposes behavior through the root entry point, and hides impl in a subfolder.
6. Verify that the rules actually work
This is the completion criterion for the entire skill: a configuration that does not fail when violated is worthless.
- Run
lint:boundaries. On the clean example it mustpass。 - Temporarily add a deep-path import to
tests/example.test.ts(for exampleimport { thing } from "../lib/impl"). Runlint:boundariesagain; it musttests-through-entrypointsreport an error withfail。 - Undo that deep-path import. Run it once more, and it mustpass。
**Completion condition:** You have observed the complete flow of "pass → fail on the deep-path import → pass again after undoing it". If step 2 did not fail, the rules are not wired in correctly; fix it before finishing.
7. Write down the conventions
Write a README.mdin the packages folder (that is, <packages-root>/README.md, placed next to the packages it governs), covering:src/packages/<name>/ directory structure (the root directory's entry point,lib/ for implementation,tests/ for tests), "only import through the package's entry point (that is, the root file)", and how to run lint:boundaries。explicitly discourage barrel files: Expose multiple small entry points instead of re-exporting an entire subtree through a single index. Keep it within the scope of "copy-paste-ready snippets + one paragraph for each of the four rules."
Then in the repository's agent-instructions file (prefer CLAUDE.md, otherwise AGENTS.md, and if neither exists, create AGENTS.md) add acontext pointerpointing to it. One line is enough, for example Packages are deep modules: see [src/packages/README.md](./src/packages/README.md) before adding or importing one. This is the key to making the agent discover it before running into the boundary rules.
Completion criteria:<packages-root>/README.md exists and discourages barrel files, and the repository's CLAUDE.md/AGENTS.md contains a link pointing to it.
Notes
- The
$1backreferences in the configuration (dependency-cruiser's group matching) are exactly what allows a package to reach into its own internals while outside packages cannot reach them. Do not split them into multiple independent per-package rules. - Public vs private is determined bydepth: a package's root files are entry points; anything in any subfolder is private. The conventional subfolders are
lib/(implementation) andtests/, but the rules do not hardcode them: any subfolder is private, so adding a new folder never requires changing the configuration. Adding a new entry point just means adding a new root file (no barrel file needed). - Packages areflat: there is only one level of direct subdirectories under the root. A package's internals can be nested to any depth; but a package cannot contain another package.
- Use
.cjs(rather than.js), so that even in a repository with"type": "module", themodule.exportsin the configuration works properly.